diff --git a/.zuul.yaml b/.zuul.yaml index 37b1a8c8ad..4a3ccf2447 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -202,6 +202,21 @@ name: keystone-dsvm-py35-functional-federation parent: keystone-dsvm-py35-functional-federation-ubuntu-xenial +# Experimental +- job: + name: keystone-dsvm-functional-oidc-federation + parent: keystone-dsvm-functional + vars: + devstack_localrc: + TEMPEST_PLUGINS: '/opt/stack/keystone-tempest-plugin' + USE_PYTHON3: True + OS_CACERT: '/opt/stack/data/ca_bundle.pem' + devstack_services: + tls-proxy: true + keystone-oidc-federation: true + devstack_plugins: + keystone: https://opendev.org/openstack/keystone + - project: templates: - openstack-cover-jobs @@ -279,3 +294,5 @@ irrelevant-files: *irrelevant-files - keystone-dsvm-py35-functional-federation-ubuntu-xenial: irrelevant-files: *irrelevant-files + - keystone-dsvm-functional-oidc-federation: + irrelevant-files: *irrelevant-files diff --git a/devstack/files/oidc/apache_oidc.conf b/devstack/files/oidc/apache_oidc.conf new file mode 100644 index 0000000000..eab84fd073 --- /dev/null +++ b/devstack/files/oidc/apache_oidc.conf @@ -0,0 +1,47 @@ +# DO NOT USE THIS IN PRODUCTION ENVIRONMENTS! +OIDCSSLValidateServer Off +OIDCOAuthSSLValidateServer Off +OIDCCookieSameSite On + +OIDCClaimPrefix "OIDC-" +OIDCResponseType "id_token" +OIDCScope "openid email profile" +OIDCProviderMetadataURL "%OIDC_METADATA_URL%" +OIDCClientID "%OIDC_CLIENT_ID%" +OIDCClientSecret "%OIDC_CLIENT_SECRET%" +OIDCPKCEMethod "S256" +OIDCCryptoPassphrase "openstack" + +OIDCRedirectURI "https://%HOST_IP%/identity/v3/auth/OS-FEDERATION/identity_providers/%IDP_ID%/protocols/openid/websso" +OIDCRedirectURI "https://%HOST_IP%/identity/v3/auth/OS-FEDERATION/websso/openid" + + + AuthType "openid-connect" + Require valid-user + LogLevel debug + + + + AuthType "openid-connect" + Require valid-user + LogLevel debug + + + + AuthType "openid-connect" + Require valid-user + LogLevel debug + + + + AuthType oauth20 + Require valid-user + + +OIDCOAuthClientID "%OIDC_CLIENT_ID%" +OIDCOAuthClientSecret "%OIDC_CLIENT_SECRET%" +OIDCOAuthIntrospectionEndpoint "%OIDC_INTROSPECTION_URL%" + +# Horizon favors the referrer to the Keystone URL that is set. +# https://github.com/openstack/horizon/blob/5e4ca1a9fdec04db08552e9e93fe372b8b8b45ae/openstack_auth/views.py#L192 +Header always set Referrer-Policy "no-referrer" diff --git a/devstack/lib/oidc.sh b/devstack/lib/oidc.sh new file mode 100644 index 0000000000..ab8731d986 --- /dev/null +++ b/devstack/lib/oidc.sh @@ -0,0 +1,160 @@ +# 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. + +DOMAIN_NAME=${DOMAIN_NAME:-federated_domain} +PROJECT_NAME=${PROJECT_NAME:-federated_project} +GROUP_NAME=${GROUP_NAME:-federated_users} + +OIDC_CLIENT_ID=${CLIENT_ID:-devstack} +OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET:-nomoresecret} + +OIDC_ISSUER=${OIDC_ISSUER:-"https://$HOST_IP:8443"} +OIDC_ISSUER_BASE="${OIDC_ISSUER}/realms/master" + +OIDC_METADATA_URL=${OIDC_METADATA_URL:-"https://$HOST_IP:8443/realms/master/.well-known/openid-configuration"} +OIDC_INTROSPECTION_URL=${OIDC_INTROSPECTION_URL:-"https://$HOST_IP:8443/realms/master/protocol/openid-connect/token/introspect"} + +IDP_ID=${IDP_ID:-sso} +IDP_USERNAME=${IDP_USERNAME:-admin} +IDP_PASSWORD=${IDP_PASSWORD:-nomoresecret} + +MAPPING_REMOTE_TYPE=${MAPPING_REMOTE_TYPE:-OIDC-preferred_username} +MAPPING_USER_NAME=${MAPPING_USER_NAME:-"{0}"} +PROTOCOL_ID=${PROTOCOL_ID:-openid} + +REDIRECT_URI="https://$HOST_IP/identity/v3/auth/OS-FEDERATION/identity_providers/$IDP_ID/protocols/openid/websso" + +OIDC_PLUGIN="$DEST/keystone/devstack" + +function install_federation { + if is_ubuntu; then + install_package libapache2-mod-auth-openidc + sudo a2enmod headers + install_package docker.io + install_package docker-compose + elif is_fedora; then + install_package mod_auth_openidc + install_package podman + install_package podman-docker + install_package docker-compose + sudo systemctl start podman.socket + else + echo "Skipping installation. Only supported on Ubuntu and RHEL based." + fi +} + +function configure_federation { + # Specify the header that contains information about the identity provider + iniset $KEYSTONE_CONF openid remote_id_attribute "HTTP_OIDC_ISS" + iniset $KEYSTONE_CONF auth methods "password,token,openid,application_credential" + iniset $KEYSTONE_CONF federation trusted_dashboard "https://$HOST_IP/auth/websso/" + + cp $DEST/keystone/etc/sso_callback_template.html /etc/keystone/ + + if [[ "$WSGI_MODE" == "uwsgi" ]]; then + restart_service "devstack@keystone" + fi + + if [[ "$OIDC_ISSUER_BASE" == "https://$HOST_IP:8443/realms/master" ]]; then + # Assuming we want to setup a local keycloak here. + sed -i "s#DEVSTACK_DEST#${DATA_DIR}#" ${OIDC_PLUGIN}/tools/oidc/docker-compose.yaml + sudo docker-compose --file ${OIDC_PLUGIN}/tools/oidc/docker-compose.yaml up -d + + # wait for the server to be up + attempt_counter=0 + max_attempts=100 + until $(curl --output /dev/null --silent --fail $OIDC_METADATA_URL); do + if [ ${attempt_counter} -eq ${max_attempts} ];then + echo "Keycloak server failed to come up in time" + exit 1 + fi + + attempt_counter=$(($attempt_counter+1)) + sleep 5 + done + + KEYCLOAK_URL="https://$HOST_IP:8443" \ + KEYCLOAK_USERNAME="admin" \ + KEYCLOAK_PASSWORD="nomoresecret" \ + HOST_IP="$HOST_IP" \ + python3 $OIDC_PLUGIN/tools/oidc/setup_keycloak_client.py + fi + + local keystone_apache_conf=$(apache_site_config_for keystone-wsgi-public) + cat $OIDC_PLUGIN/files/oidc/apache_oidc.conf | sudo tee -a $keystone_apache_conf + sudo sed -i -e " + s|%OIDC_CLIENT_ID%|$OIDC_CLIENT_ID|g; + s|%OIDC_CLIENT_SECRET%|$OIDC_CLIENT_SECRET|g; + s|%OIDC_METADATA_URL%|$OIDC_METADATA_URL|g; + s|%OIDC_INTROSPECTION_URL%|$OIDC_INTROSPECTION_URL|g; + s|%HOST_IP%|$HOST_IP|g; + s|%IDP_ID%|$IDP_ID|g; + " $keystone_apache_conf + + restart_apache_server +} + +function register_federation { + local federated_domain=$(get_or_create_domain $DOMAIN_NAME) + local federated_project=$(get_or_create_project $PROJECT_NAME $DOMAIN_NAME) + local federated_users=$(get_or_create_group $GROUP_NAME $DOMAIN_NAME) + local member_role=$(get_or_create_role Member) + + openstack role add --group $federated_users --domain $federated_domain $member_role + openstack role add --group $federated_users --project $federated_project $member_role + + openstack identity provider create \ + --remote-id $OIDC_ISSUER_BASE \ + --domain $DOMAIN_NAME $IDP_ID +} + +function configure_tests_settings { + # Here we set any settings that might be need by the fed_scenario set of tests + iniset $TEMPEST_CONFIG identity-feature-enabled federation True + + # we probably need an oidc version of this flag based on local oidc + iniset $TEMPEST_CONFIG identity-feature-enabled external_idp True + + # Identity provider settings + iniset $TEMPEST_CONFIG fed_scenario idp_id $IDP_ID + iniset $TEMPEST_CONFIG fed_scenario idp_remote_ids $OIDC_ISSUER_BASE + iniset $TEMPEST_CONFIG fed_scenario idp_username $IDP_USERNAME + iniset $TEMPEST_CONFIG fed_scenario idp_password $IDP_PASSWORD + iniset $TEMPEST_CONFIG fed_scenario idp_oidc_url $OIDC_ISSUER + iniset $TEMPEST_CONFIG fed_scenario idp_client_id $OIDC_CLIENT_ID + iniset $TEMPEST_CONFIG fed_scenario idp_client_secret $OIDC_CLIENT_SECRET + + # Mapping rules settings + iniset $TEMPEST_CONFIG fed_scenario mapping_remote_type $MAPPING_REMOTE_TYPE + iniset $TEMPEST_CONFIG fed_scenario mapping_user_name $MAPPING_USER_NAME + iniset $TEMPEST_CONFIG fed_scenario mapping_group_name $GROUP_NAME + iniset $TEMPEST_CONFIG fed_scenario mapping_group_domain_name $DOMAIN_NAME + iniset $TEMPEST_CONFIG fed_scenario enable_k2k_groups_mapping False + + # Protocol settings + iniset $TEMPEST_CONFIG fed_scenario protocol_id $PROTOCOL_ID +} + +function uninstall_federation { + # Ensure Keycloak is stopped and the containers are cleaned up + sudo docker-compose --file ${OIDC_PLUGIN}/tools/oidc/docker-compose.yaml down + if is_ubuntu; then + sudo docker rmi $(sudo docker images -a -q) + uninstall_package docker-compose + elif is_fedora; then + sudo podman rmi $(sudo podman images -a -q) + uninstall_package podman + else + echo "Skipping uninstallation of OIDC federation for non ubuntu nor fedora nor suse host" + fi +} + diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 8f7a385357..eca1d1ac0f 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -14,7 +14,13 @@ # under the License. KEYSTONE_PLUGIN=$DEST/keystone/devstack -source $KEYSTONE_PLUGIN/lib/federation.sh + +if is_service_enabled keystone-saml2-federation; then + source $KEYSTONE_PLUGIN/lib/federation.sh +elif is_service_enabled keystone-oidc-federation; then + source $KEYSTONE_PLUGIN/lib/oidc.sh +fi + source $KEYSTONE_PLUGIN/lib/scope.sh # For more information on Devstack plugins, including a more detailed @@ -25,6 +31,10 @@ if [[ "$1" == "stack" && "$2" == "install" ]]; then # This phase is executed after the projects have been installed echo "Keystone plugin - Install phase" if is_service_enabled keystone-saml2-federation; then + echo "installing saml2 federation" + install_federation + elif is_service_enabled keystone-oidc-federation; then + echo "installing oidc federation" install_federation fi @@ -33,6 +43,10 @@ elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then # before they are started echo "Keystone plugin - Post-config phase" if is_service_enabled keystone-saml2-federation; then + echo "configuring saml2 federation" + configure_federation + elif is_service_enabled keystone-oidc-federation; then + echo "configuring oidc federation" configure_federation fi @@ -40,12 +54,21 @@ elif [[ "$1" == "stack" && "$2" == "extra" ]]; then # This phase is executed after the projects have been started echo "Keystone plugin - Extra phase" if is_service_enabled keystone-saml2-federation; then + echo "registering saml2 federation" + register_federation + elif is_service_enabled keystone-oidc-federation; then + echo "registering oidc federation" register_federation fi + elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then # This phase is executed after Tempest was configured echo "Keystone plugin - Test-config phase" if is_service_enabled keystone-saml2-federation; then + echo "config tests settings for saml" + configure_tests_settings + elif is_service_enabled keystone-oidc-federation; then + echo "config tests settings for oidc" configure_tests_settings fi if [[ "$(trueorfalse False KEYSTONE_ENFORCE_SCOPE)" == "True" ]] ; then @@ -66,6 +89,10 @@ if [[ "$1" == "clean" ]]; then # Called by clean.sh after the "unstack" phase # Undo what was performed during the "install" phase if is_service_enabled keystone-saml2-federation; then + echo "uninstalling saml" + uninstall_federation + elif is_service_enabled keystone-oidc-federation; then + echo "uninstalling oidc" uninstall_federation fi fi diff --git a/devstack/tools/oidc/__init__.py b/devstack/tools/oidc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/devstack/tools/oidc/docker-compose.yaml b/devstack/tools/oidc/docker-compose.yaml new file mode 100644 index 0000000000..6e4a428c96 --- /dev/null +++ b/devstack/tools/oidc/docker-compose.yaml @@ -0,0 +1,33 @@ +version: "3" + +services: + keycloak: + image: quay.io/keycloak/keycloak:latest + command: start-dev --log-level debug --log=console,file --https-certificate-file=/etc/certs/devstack-cert.pem --https-certificate-key-file=/etc/certs/devstack-cert.pem + container_name: oidc_keycloak_1 + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: nomoresecret + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: nomoresecret + KEYCLOAK_LOG_LEVEL: DEBUG + DB_VENDOR: mariadb + DB_DATABASE: keycloak + DB_USER: keycloak + DB_PASSWORD: "nomoresecret" + DB_ADDR: "keycloak-database" + DB_PORT: "3306" + JAVA_OPTS: "-server -Xms128m -Xmx1024m -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true" + ports: + - "8088:8080" # host:container + - "8443:8443" + volumes: + - DEVSTACK_DEST:/etc/certs:rw + + keycloak-database: + image: quay.io/metal3-io/mariadb:latest + environment: + MYSQL_ROOT_PASSWORD: nomoresecret + MYSQL_DATABASE: keycloak + MYSQL_USER: keycloak + MYSQL_PASSWORD: nomoresecret diff --git a/devstack/tools/oidc/setup_keycloak_client.py b/devstack/tools/oidc/setup_keycloak_client.py new file mode 100644 index 0000000000..15fa37b41f --- /dev/null +++ b/devstack/tools/oidc/setup_keycloak_client.py @@ -0,0 +1,61 @@ +import os +import requests + +KEYCLOAK_USERNAME = os.environ.get('KEYCLOAK_USERNAME') +KEYCLOAK_PASSWORD = os.environ.get('KEYCLOAK_PASSWORD') +KEYCLOAK_URL = os.environ.get('KEYCLOAK_URL') +HOST_IP = os.environ.get('HOST_IP', 'localhost') + +class KeycloakClient(object): + def __init__(self): + self.session = requests.session() + + @staticmethod + def construct_url(realm, path): + return f'{KEYCLOAK_URL}/admin/realms/{realm}/{path}' + + @staticmethod + def token_endpoint(realm): + return f'{KEYCLOAK_URL}/realms/{realm}/protocol/openid-connect/token' + + def _admin_auth(self, realm): + params = { + 'grant_type': 'password', + 'client_id': 'admin-cli', + 'username': KEYCLOAK_USERNAME, + 'password': KEYCLOAK_PASSWORD, + 'scope': 'openid', + } + r = requests.post(self.token_endpoint(realm), data=params).json() + headers = { + 'Authorization': ("Bearer %s" % r['access_token']), + 'Content-Type': 'application/json' + } + self.session.headers.update(headers) + return r + + def create_client(self, realm, client_id, client_secret, redirect_uris): + self._admin_auth(realm) + data = { + 'clientId': client_id, + 'secret': client_secret, + 'redirectUris': redirect_uris, + 'implicitFlowEnabled': True, + 'directAccessGrantsEnabled': True, + } + return self.session.post(self.construct_url(realm, 'clients'), json=data) + + +def main(): + c = KeycloakClient() + + redirect_uris = [ + f'http://{HOST_IP}/identity/v3/auth/OS-FEDERATION/identity_providers/sso/protocols/openid/websso', + f'http://{HOST_IP}/identity/v3/auth/OS-FEDERATION/websso/openid' + ] + + c.create_client('master', 'devstack', 'nomoresecret', redirect_uris) + + +if __name__ == "__main__": + main()