From 57902730d633e73c1250539afb5762bef1a03493 Mon Sep 17 00:00:00 2001 From: Qibin Yao Date: Mon, 1 Aug 2022 16:32:34 +0900 Subject: [PATCH] Add OpenID Connect Token Auth for k8s This patch adds openid token auth support when calling k8s APIs. Openid token auth of k8s relies on an external openid provider, and Keycloak acts as the openid provider in this implementation. Implements: blueprint support-openid-k8s-vim Change-Id: Ie5e080a20cba3ba0ed514ede7955eb16729d797c --- .zuul.yaml | 14 + doc/source/reference/index.rst | 1 + ...bernetes_openid_token_auth_usage_guide.rst | 322 +++ doc/source/reference/vim_config.rst | 85 +- .../etsi_containerized_vnf_usage_guide.rst | 64 +- playbooks/devstack/pre.yaml | 1 + ...pport-openid-k8s-vim-8767a454e6b0a72d.yaml | 7 + roles/setup-default-vim/tasks/main.yaml | 72 +- roles/setup-k8s-oidc/defaults/main.yaml | 1 + .../files/cluster_role_binding.yaml | 11 + roles/setup-k8s-oidc/files/create_keycloak.sh | 10 + .../setup-k8s-oidc/files/generate_ssl_cert.sh | 27 + .../setup-k8s-oidc/files/import_oidc_realm.sh | 19 + roles/setup-k8s-oidc/files/oidc_realm.json | 1843 +++++++++++++++++ roles/setup-k8s-oidc/files/ssl_csr.conf | 12 + roles/setup-k8s-oidc/tasks/main.yaml | 109 + tacker/common/container/kubernetes_utils.py | 36 +- tacker/common/oidc_utils.py | 57 + tacker/db/nfvo/nfvo_db.py | 5 +- tacker/db/nfvo/nfvo_db_plugin.py | 17 +- tacker/extensions/vnfm.py | 5 + tacker/nfvo/drivers/vim/kubernetes_driver.py | 8 + tacker/nfvo/nfvo_plugin.py | 32 +- tacker/sol_refactored/common/exceptions.py | 5 + tacker/sol_refactored/common/oidc_utils.py | 57 + tacker/sol_refactored/common/vim_utils.py | 10 +- .../kubernetes/kubernetes_utils.py | 41 +- .../tests/etc/samples/local-k8s-vim-oidc.yaml | 9 + .../sol_kubernetes_oidc_auth/__init__.py | 0 .../sol_kubernetes_oidc_auth/test_vim.py | 147 ++ .../vnflcm_v1/__init__.py | 0 .../vnflcm_v1/test_kubernetes_oidc_auth.py | 52 + .../vnflcm_v2/__init__.py | 0 .../vnflcm_v2/base_v2.py | 89 + .../vnflcm_v2/test_kubernetes_oidc_auth.py | 255 +++ .../functional/sol_kubernetes_v2/paramgen.py | 21 + .../tests/unit/common/container/__init__.py | 0 .../common/container/test_kubernetes_utils.py | 89 +- tacker/tests/unit/common/test_oidc_utils.py | 123 ++ tacker/tests/unit/nfvo/test_nfvo_plugin.py | 314 +++ .../sol_refactored/common/test_oidc_utils.py | 123 ++ .../sol_refactored/common/test_vim_utils.py | 16 + tacker/tests/unit/vnfm/test_vim_client.py | 29 + tacker/vnfm/vim_client.py | 7 + tools/test-setup-k8s-vim.sh | 33 +- tox.ini | 6 + 46 files changed, 4133 insertions(+), 51 deletions(-) create mode 100644 doc/source/reference/kubernetes_openid_token_auth_usage_guide.rst create mode 100644 releasenotes/notes/support-openid-k8s-vim-8767a454e6b0a72d.yaml create mode 100644 roles/setup-k8s-oidc/defaults/main.yaml create mode 100644 roles/setup-k8s-oidc/files/cluster_role_binding.yaml create mode 100644 roles/setup-k8s-oidc/files/create_keycloak.sh create mode 100644 roles/setup-k8s-oidc/files/generate_ssl_cert.sh create mode 100644 roles/setup-k8s-oidc/files/import_oidc_realm.sh create mode 100644 roles/setup-k8s-oidc/files/oidc_realm.json create mode 100644 roles/setup-k8s-oidc/files/ssl_csr.conf create mode 100644 roles/setup-k8s-oidc/tasks/main.yaml create mode 100644 tacker/common/oidc_utils.py create mode 100644 tacker/sol_refactored/common/oidc_utils.py create mode 100644 tacker/tests/etc/samples/local-k8s-vim-oidc.yaml create mode 100644 tacker/tests/functional/sol_kubernetes_oidc_auth/__init__.py create mode 100644 tacker/tests/functional/sol_kubernetes_oidc_auth/test_vim.py create mode 100644 tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v1/__init__.py create mode 100644 tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v1/test_kubernetes_oidc_auth.py create mode 100644 tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/__init__.py create mode 100644 tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/base_v2.py create mode 100644 tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/test_kubernetes_oidc_auth.py create mode 100644 tacker/tests/unit/common/container/__init__.py create mode 100644 tacker/tests/unit/common/test_oidc_utils.py create mode 100644 tacker/tests/unit/sol_refactored/common/test_oidc_utils.py diff --git a/.zuul.yaml b/.zuul.yaml index c3c073369..a1e110080 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -568,6 +568,19 @@ kubernetes_vim_rsc_wait_timeout: 800 tox_envlist: dsvm-functional-sol-kubernetes-v2 +- job: + name: tacker-functional-devstack-kubernetes-oidc-auth + parent: tacker-functional-devstack-multinode-sol-kubernetes-v2 + description: | + Multinodes job for Kubernetes OIDC Auth tests + host-vars: + controller-tacker: + tox_envlist: dsvm-functional-sol_kubernetes_oidc_auth + vars: + keycloak_host: "{{ hostvars['controller-k8s']['nodepool']['private_ipv4'] }}" + keycloak_http_port: 8080 + keycloak_https_port: 8443 + - job: name: tacker-compliance-devstack-multinode-sol parent: tacker-functional-devstack-multinode-legacy @@ -597,4 +610,5 @@ - tacker-functional-devstack-multinode-sol-kubernetes-v2 - tacker-functional-devstack-multinode-sol-multi-tenant - tacker-functional-devstack-multinode-sol-kubernetes-multi-tenant + - tacker-functional-devstack-kubernetes-oidc-auth - tacker-compliance-devstack-multinode-sol diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst index 4e6dba7c6..d8085cf0a 100644 --- a/doc/source/reference/index.rst +++ b/doc/source/reference/index.rst @@ -26,3 +26,4 @@ Reference block_storage_usage_guide.rst reservation_policy_usage_guide.rst maintenance_usage_guide.rst + kubernetes_openid_token_auth_usage_guide.rst diff --git a/doc/source/reference/kubernetes_openid_token_auth_usage_guide.rst b/doc/source/reference/kubernetes_openid_token_auth_usage_guide.rst new file mode 100644 index 000000000..d0fe4bb9e --- /dev/null +++ b/doc/source/reference/kubernetes_openid_token_auth_usage_guide.rst @@ -0,0 +1,322 @@ +============================================ +Kubernetes VIM OpenID Token Auth Usage Guide +============================================ + +Overview +-------- +Kubernetes has multiple authentication strategies. This document describes +how Tacker use `OpenID token`_ to authenticate with Kubernetes. + +The OpenID token authentication of Kubernetes relies on an external OpenID +provider, and `Keycloak`_ acts as the OpenID provider in this document. + +Preparation +----------- + +Prerequisites +~~~~~~~~~~~~~ + +The following packages should be installed: + +* Kubernetes +* Docker + +Start Keycloak +~~~~~~~~~~~~~~ + +Create a CSR config file ``csr.cnf``: + +.. code-block:: cfg + + [req] + req_extensions = v3_req + distinguished_name = req_distinguished_name + [req_distinguished_name] + + [ v3_req ] + basicConstraints = CA:FALSE + keyUsage = nonRepudiation, digitalSignature, keyEncipherment + subjectAltName = @alt_names + + [alt_names] + IP.1 = 127.0.0.1 + IP.2 = 192.168.2.33 # Host IP + +Generate SSL certificate for Keycloak: + +.. code-block:: shell + + #!/bin/bash + + req_conf=csr.cnf + ssl_dir=/etc/keycloak/ssl + key_file=$ssl_dir/keycloak.key + csr_file=$ssl_dir/keycloak.csr + crt_file=$ssl_dir/keycloak.crt + + k8s_ssl_dir=/etc/kubernetes/pki + k8s_ca_crt=$k8s_ssl_dir/ca.crt + k8s_ca_key=$k8s_ssl_dir/ca.key + + # make a directory for storing certificate + mkdir -p $ssl_dir + + # generate private key + openssl genrsa -out $key_file 2048 + + # generate certificate signing request + openssl req -new -key $key_file -out $csr_file -subj "/CN=Keycloak" \ + -config $req_conf + + # use Kubernetes's CA for issuing certificate + openssl x509 -req -in $csr_file -CA $k8s_ca_crt -CAkey $k8s_ca_key \ + -CAcreateserial -out $crt_file -days 365 -extensions v3_req \ + -extfile $req_conf + + # add executeable permission to key file + chmod 755 $key_file + +Starts a Keycloak container with docker: + +.. code-block:: console + + $ docker run -d \ + --net=host \ + -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \ + -e KC_HTTP_PORT=8080 -e KC_HTTPS_PORT=8443 \ + -e KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/keycloak.crt \ + -e KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/keycloak.key \ + -v /etc/keycloak/ssl:/opt/keycloak/conf \ + quay.io/keycloak/keycloak:18.0.2 \ + start-dev + +Setup Keycloak +~~~~~~~~~~~~~~ + +Login to the admin console + +* Open the URL https://192.168.2.33:8443/admin with a browser +* Fill in the form with the following values: + + + :guilabel:`Username`: **admin** + + :guilabel:`Password`: **admin** + +Create a realm + +* Click :guilabel:`Add realm` +* Fill in the :guilabel:`Name` with **oidc** +* Click :guilabel:`Create` + + +Create a user + +* Click :guilabel:`Users` in the menu +* Click :guilabel:`Add user` +* Fill in the :guilabel:`Username` with **end-user** +* Click :guilabel:`Save` + +Set user credentials + +* Click :guilabel:`Credentials` +* Fill in the form with the following values: + + + :guilabel:`Password` : **end-user** + + :guilabel:`Password Confirmation` : **end-user** + +* Turn off Temporary +* Click :guilabel:`Save` + +Set user attributes + +* Click :guilabel:`Attributes` +* Fill in the form with the following values: + + + :guilabel:`Key` : **name** + + :guilabel:`Value` : **end-user** + +* Click :guilabel:`Add` +* Click :guilabel:`Save` + +Create a client + +* Click :guilabel:`Clients` in the menu +* Click :guilabel:`Create` +* Fill in the :guilabel:`Client ID` with **tacker** +* Click :guilabel:`Save` + +Set client type and valid redirect URIs + +* Click :guilabel:`Settings` +* Select **confidential** as the :guilabel:`Access Type` +* Fill in the :guilabel:`Valid Redirect URIs` with **http://*** +* Click :guilabel:`Save` + +Set client mappers + +* Click :guilabel:`Mappers` +* Click :guilabel:`Create` +* Select **User Attribute** as the :guilabel:`Mapper Type` +* Fill in the form with the following values: + + + :guilabel:`Name` : **name** + + :guilabel:`User Attribute` : **name** + + :guilabel:`Token Claim Name` : **name** + +* Select **String** as the :guilabel:`Claim JSON Type` +* Click :guilabel:`Save` + +View client secret + +* Click :guilabel:`Credentials` and view the secret. + +Setup Kubernetes +~~~~~~~~~~~~~~~~ + +Add oidc related startup parameters to kube-apiserver manifest file. + +.. code-block:: console + + $ cat /etc/kubernetes/manifests/kube-apiserver.yaml + + spec: + containers: + - command: + - kube-apiserver + - --oidc-issuer-url=https://192.168.2.33:8443/realms/oidc + - --oidc-client-id=tacker + - --oidc-username-claim=name + - --oidc-username-prefix=- + - --oidc-ca-file=/etc/kubernetes/ssl/ca.crt + +.. note:: + After modifying kube-apiserver manifest file, the kube-apiserver will be restarted. + +Create a cluster role binding to grant end users permissions to manipulate +Kubernetes resources. + +* Create a cluster role binding file ``cluster_role_binding.yaml``: + + .. code-block:: yaml + + kind: ClusterRoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + metadata: + name: oidc-cluster-admin-binding + roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io + subjects: + - kind: User + name: end-user + +* Create cluster role binding: + + .. code-block:: console + + $ kubectl create -f cluster_role_binding.yaml + + +Verify +~~~~~~ + +Get token endpoint from Keycloak: + +.. code-block:: console + + $ curl -ks -X GET https://192.168.2.33:8443/realms/oidc/.well-known/openid-configuration | jq -r .token_endpoint + +Result: + +.. code-block:: console + + https://192.168.2.33:8443/realms/oidc/protocol/openid-connect/token + +Get a OpenID token from Keycloak: + +.. code-block:: console + + $ ID_TOKEN=$(curl -ks -X POST https://192.168.2.33:8443/realms/oidc/protocol/openid-connect/token \ + -d grant_type=password -d scope=openid -d username=end-user -d password=end-user \ + -d client_id=tacker -d client_secret=A93HfOUpySm6BjPug9PJdJumjEGUJMhc | jq -r .id_token) + $ echo $ID_TOKEN + +Result: + +.. code-block:: console + + eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxbC1RMy1KanQ1eHVrNzhYbUVkZDU2Mko4YXRRVF95MU1zS0JDUTBBcklnIn0.eyJleHAiOjE2NjAxMjExNTUsImlhdCI6MTY2MDEyMDg1NSwiYXV0aF90aW1lIjowLCJqdGkiOiIwZjdkNDE2My05Njk1LTQ3MGMtYmE1OC02MWI4NDM4YTU4MzQiLCJpc3MiOiJodHRwczovLzE5Mi4xNjguMi4zMzo4NDQzL3JlYWxtcy9vaWRjIiwiYXVkIjoidGFja2VyIiwic3ViIjoiOGZlZDVhYzctZDY4OS00NWM1LWE5NmQtYTlmN2M3Y2QxZTJjIiwidHlwIjoiSUQiLCJhenAiOiJ0YWNrZXIiLCJzZXNzaW9uX3N0YXRlIjoiNzhiYzhmNDEtMjc4NC00YTU5LWJjOTUtNjNkZDM5YTQ5NjNiIiwiYXRfaGFzaCI6Ik9WczJ3Q29VclU0QWxZaml0dGNQLXciLCJhY3IiOiIxIiwic2lkIjoiNzhiYzhmNDEtMjc4NC00YTU5LWJjOTUtNjNkZDM5YTQ5NjNiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJlbmQtdXNlciJ9.SAncMlpRCLY8JlRUZ7YwirmYs5Qc-5qrvJYmyCZSiRgHyn-7cuqNan3vyVGO46Iv9Da51_Im3L5HaVJTcReeCZ2fhuzgei3yOquPugcfaqKKZEujA042Cc0pFTLS_dPl1xX3XINEcN4nGYGhGtLi8CBH0iANi-IY_VEdxogTyc9MlKgjP9Ca8eYNUPhop49GwLC-ph5vMShS9O834ywtQargb51zokQsoXAYrGBJMTWr37uMxP7UWXpYAQa82OyX3fElpueurd5WGEzGT1AhN1Ad4uIAxgD6dxFsiQYOHRSH-sByV0IwMdZoqIm4GFS6NHLj5usr6PSA5U9QpgCI7Q + +Get all namespaces with OpenID token from Kubernetes: + +.. code-block:: console + + curl -ks -H "Authorization: Bearer $ID_TOKEN" https://192.168.2.33:6443/api/v1/namespaces + +Result: + +.. code-block:: console + + { + "kind": "NamespaceList", + "apiVersion": "v1", + ...omit... + +View certificates +~~~~~~~~~~~~~~~~~ + +View Kubernetes CA certificate: + +.. code-block:: console + + $ cat /etc/kubernetes/pki/ca.crt + -----BEGIN CERTIFICATE----- + MIICwjCCAaqgAwIBAgIBADANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdrdWJl + LWNhMB4XDTIwMDgyNjA5MzIzMVoXDTMwMDgyNDA5MzIzMVowEjEQMA4GA1UEAxMH + a3ViZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxkeE16lPAd + pfJj5GJMvZJFcX/CD6EB/LUoKwGmqVoOUQPd3b/NGy+qm+3bO9EU73epUPsVaWk2 + Lr+Z1ua7u+iib/OMsfsSXMZ5OEPgd8ilrTGhXOH8jDkif9w1NtooJxYSRcHEwxVo + +aXdIJhqKdw16NVP/elS9KODFdRZDfQ6vU5oHSg3gO49kgv7CaxFdkF7QEHbchsJ + 0S1nWMPAlUhA5b8IAx0+ecPlMYUGyGQIQgjgtHgeawJebH3PWy32UqfPhkLPzxsy + TSxk6akiXJTg6mYelscuxPLSe9UqNvHRIUoad3VnkF3+0CJ1z0qvfWIrzX3w92/p + YsDBZiP6vi8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMB + Af8wDQYJKoZIhvcNAQELBQADggEBAIbv2ulEcQi019jKz4REy7ZyH8+ExIUBBuIz + InAkfxNNxV83GkdyA9amk+LDoF/IFLMltAMM4b033ZKO5RPrHoDKO+xCA0yegYqU + BViaUiEXIvi/CcDpT9uh2aNO8wX5T/B0WCLfWFyiK+rr9qcosFYxWSdU0kFeg+Ln + YAaeFY65ZWpCCyljGpr2Vv11MAq1Tws8rEs3rg601SdKhBmkgcTAcCzHWBXR1P8K + rfzd6h01HhIomWzM9xrP2/2KlYRvExDLpp9qwOdMSanrszPDuMs52okXgfWnEqlB + 2ZrqgOcTmyFzFh9h2dj1DJWvCvExybRmzWK1e8JMzTb40MEApyY= + -----END CERTIFICATE----- + +View Keycloak certificate: + +.. code-block:: console + + $ cat /etc/keycloak/ssl/keycloak.crt + -----BEGIN CERTIFICATE----- + MIIC7TCCAdWgAwIBAgIUQK2k5uNvlRLx43LI/t3a2/A/3iQwDQYJKoZIhvcNAQEL + BQAwFTETMBEGA1UEAxMKa3ViZXJuZXRlczAeFw0yMjA4MDQwNjIwNTFaFw0yMzA4 + MDQwNjIwNTFaMBMxETAPBgNVBAMMCEtleWNsb2FrMIIBIjANBgkqhkiG9w0BAQEF + AAOCAQ8AMIIBCgKCAQEAni7HWLn2IpUImGO1sbBf/XuqATkXSeIIRuQuFymwYPoX + BP7RowzrbfF9KUwdIKlz9IXjqb1hplumiqNy1Sc7MmrTY9Fj87MNAMlnCIvyWkjE + XVXWxGef49mqc85P2K1iuAsr2R7sDrv7SC0ch+lHclOjGDmCjKOk8qF3kD1LATWg + zf42aXb4nNF9kyIOPEbI+jX4PWhAQpEz5nIG+xIRjTHGfacjpeg0+XOK21wLAuQB + fqebJ6GxX4OzB37ZtLLgrKyBYWaWuYkWbexVRM3wEvQu8ENkvhV017iPuPHSxNWx + Y8z072XMs9j8XRQD65EVqObXyizotPRJF4slEJ9qMQIDAQABozcwNTAJBgNVHRME + AjAAMAsGA1UdDwQEAwIF4DAbBgNVHREEFDAShwR/AAABhwTAqAIhhwQKCgCMMA0G + CSqGSIb3DQEBCwUAA4IBAQBebjmNHd8sJXjvPQc3uY/3KSDpk9AYfYzhUZvcvLNg + z0llFqXHaFlMqHTsz1tOH4Ns4PDKKoRT0JIKC1FkvjzqgL+X2jWFS0NRoNyd3W3B + yHLEL7MdQqDR+tZX02EGfaGXjuy8GHIU4J2hXhohmpn6ntfiRONfY8jaEjIecPFS + IwZWXNhsDESa1zuDe0PatES/Ati8bAUpN2rb/7rsE/AeM5GXpQfOKV0XxdIeBZ82 + Vf5cUDWPipvq2Q9KS+yrTvEObGtA6gKhQ4bpz3MieU3N8AtQpEKtROH7mJWMHyl2 + roD1k8KeJlfvR/XcVTGFcgIdNLfKIdd99Xfi4gSaIKuw + -----END CERTIFICATE----- + +Deploy CNF with OpenID token +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Please refer to `CNF usage guide`_ to deploy CNF with OpenID token. + +.. _OpenID token : https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens +.. _Keycloak: https://www.keycloak.org +.. _CNF usage guide: https://docs.openstack.org/tacker/latest/user/etsi_containerized_vnf_usage_guide.html diff --git a/doc/source/reference/vim_config.rst b/doc/source/reference/vim_config.rst index c8e0c4e29..08bdb8c06 100644 --- a/doc/source/reference/vim_config.rst +++ b/doc/source/reference/vim_config.rst @@ -166,10 +166,12 @@ Cert Verify Kubernetes ---------- -You configure Kubernetes VIM with parameters retrieved from ``kubectl`` command -as described in +You configure Kubernetes VIM with parameters retrieved +from ``kubectl`` command as described in :doc:`/install/kubernetes_vim_installation`. -Here is an example of Kubernetes VIM configuration. + +1. This is an example of Kubernetes VIM configuration with +Service Account Token. .. code-block:: yaml @@ -195,6 +197,56 @@ Here is an example of Kubernetes VIM configuration. -----END CERTIFICATE-----" type: "kubernetes" +2. Another example of Kubernetes VIM configuration with +OpenID Connect Token. The OpenID Connect related parameters are described in +:doc:`kubernetes_openid_token_auth_usage_guide`. + +.. code-block:: yaml + + auth_url: "https://192.168.33.100:6443" + project_name: "default" + oidc_token_url: "https://192.168.33.100:8443/realms/oidc/protocol/openid-connect/token" + client_id: "tacker" + client_secret: "A93HfOUpySm6BjPug9PJdJumjEGUJMhc" + username: "end-user" + password: "end-user" + ssl_ca_cert: "-----BEGIN CERTIFICATE----- + MIICwjCCAaqgAwIBAgIBADANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdrdWJl + LWNhMB4XDTIwMDgyNjA5MzIzMVoXDTMwMDgyNDA5MzIzMVowEjEQMA4GA1UEAxMH + a3ViZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxkeE16lPAd + pfJj5GJMvZJFcX/CD6EB/LUoKwGmqVoOUQPd3b/NGy+qm+3bO9EU73epUPsVaWk2 + Lr+Z1ua7u+iib/OMsfsSXMZ5OEPgd8ilrTGhXOH8jDkif9w1NtooJxYSRcHEwxVo + +aXdIJhqKdw16NVP/elS9KODFdRZDfQ6vU5oHSg3gO49kgv7CaxFdkF7QEHbchsJ + 0S1nWMPAlUhA5b8IAx0+ecPlMYUGyGQIQgjgtHgeawJebH3PWy32UqfPhkLPzxsy + TSxk6akiXJTg6mYelscuxPLSe9UqNvHRIUoad3VnkF3+0CJ1z0qvfWIrzX3w92/p + YsDBZiP6vi8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMB + Af8wDQYJKoZIhvcNAQELBQADggEBAIbv2ulEcQi019jKz4REy7ZyH8+ExIUBBuIz + InAkfxNNxV83GkdyA9amk+LDoF/IFLMltAMM4b033ZKO5RPrHoDKO+xCA0yegYqU + BViaUiEXIvi/CcDpT9uh2aNO8wX5T/B0WCLfWFyiK+rr9qcosFYxWSdU0kFeg+Ln + YAaeFY65ZWpCCyljGpr2Vv11MAq1Tws8rEs3rg601SdKhBmkgcTAcCzHWBXR1P8K + rfzd6h01HhIomWzM9xrP2/2KlYRvExDLpp9qwOdMSanrszPDuMs52okXgfWnEqlB + 2ZrqgOcTmyFzFh9h2dj1DJWvCvExybRmzWK1e8JMzTb40MEApyY= + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIC7TCCAdWgAwIBAgIUQK2k5uNvlRLx43LI/t3a2/A/3iQwDQYJKoZIhvcNAQEL + BQAwFTETMBEGA1UEAxMKa3ViZXJuZXRlczAeFw0yMjA4MDQwNjIwNTFaFw0yMzA4 + MDQwNjIwNTFaMBMxETAPBgNVBAMMCEtleWNsb2FrMIIBIjANBgkqhkiG9w0BAQEF + AAOCAQ8AMIIBCgKCAQEAni7HWLn2IpUImGO1sbBf/XuqATkXSeIIRuQuFymwYPoX + BP7RowzrbfF9KUwdIKlz9IXjqb1hplumiqNy1Sc7MmrTY9Fj87MNAMlnCIvyWkjE + XVXWxGef49mqc85P2K1iuAsr2R7sDrv7SC0ch+lHclOjGDmCjKOk8qF3kD1LATWg + zf42aXb4nNF9kyIOPEbI+jX4PWhAQpEz5nIG+xIRjTHGfacjpeg0+XOK21wLAuQB + fqebJ6GxX4OzB37ZtLLgrKyBYWaWuYkWbexVRM3wEvQu8ENkvhV017iPuPHSxNWx + Y8z072XMs9j8XRQD65EVqObXyizotPRJF4slEJ9qMQIDAQABozcwNTAJBgNVHRME + AjAAMAsGA1UdDwQEAwIF4DAbBgNVHREEFDAShwR/AAABhwTAqAIhhwQKCgCMMA0G + CSqGSIb3DQEBCwUAA4IBAQBebjmNHd8sJXjvPQc3uY/3KSDpk9AYfYzhUZvcvLNg + z0llFqXHaFlMqHTsz1tOH4Ns4PDKKoRT0JIKC1FkvjzqgL+X2jWFS0NRoNyd3W3B + yHLEL7MdQqDR+tZX02EGfaGXjuy8GHIU4J2hXhohmpn6ntfiRONfY8jaEjIecPFS + IwZWXNhsDESa1zuDe0PatES/Ati8bAUpN2rb/7rsE/AeM5GXpQfOKV0XxdIeBZ82 + Vf5cUDWPipvq2Q9KS+yrTvEObGtA6gKhQ4bpz3MieU3N8AtQpEKtROH7mJWMHyl2 + roD1k8KeJlfvR/XcVTGFcgIdNLfKIdd99Xfi4gSaIKuw + -----END CERTIFICATE-----" + type: "kubernetes" + Auth URL ~~~~~~~~ @@ -214,8 +266,35 @@ Use SSL CA Cert ~~~~~~~~~~~~~~~ The value of SSL CA Cert for X.509 client authentication. It can be ``None``. +The SSL certificates of Kubernetes and OpenID provider should be concatenated +with a newline if both are needed. Type ~~~~ Type of VIM to specify it explicitly as ``kubernetes``. + +OpenID Token URL +~~~~~~~~~~~~~~~~ + +Token Endpoint URL of OpenID provider. + +Client ID +~~~~~~~~~ + +The name of Relying Party(client). + +Client Secret +~~~~~~~~~~~~~ + +The secret of Relying Party(client). + +Username +~~~~~~~~ + +The name of End-user. + +Password +~~~~~~~~ + +The password of End-user. diff --git a/doc/source/user/etsi_containerized_vnf_usage_guide.rst b/doc/source/user/etsi_containerized_vnf_usage_guide.rst index 8c6d27c9d..6c061cadf 100644 --- a/doc/source/user/etsi_containerized_vnf_usage_guide.rst +++ b/doc/source/user/etsi_containerized_vnf_usage_guide.rst @@ -57,7 +57,7 @@ please refer to [#first]_. auth_url: "https://192.168.33.100:6443" project_name: "default" bearer_token: "eyJhbGciOiJSUzI1NiIsImtpZCI6IlBRVDgxQkV5VDNVR1M1WGEwUFYxSXFkZFhJWDYzNklvMEp2WklLMnNFdk0ifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi10b2tlbi12cnpoaiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJhZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImNhY2VmMzEzLTMzYjYtNDQ5MS1iMWUyLTg0NmQ2N2E0OTdkNSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTphZG1pbiJ9.R76VIWVZnQxa9NG02HIqux1xTJG4i7dkXsp52T4UU8bvNfsfi18kW_p3ZvaNTxw0yABBcmkYZoOBe4MNP5cTP6TtR_ERZoA5QCViasW_u36rSTBT0-MHRPbkXjJYetzYaFYUO-DlJd3194yOtVHtrxUd8D31qw0f1FlP8BHxblDjZkYlgYSjHCxcwEdwlnYaa0SiH2kl6_oCBRFg8cUfXDeTOmH9XEfdrJ6ubJ4OyqG6YjfiKDDiEHgIehy7s7vZGVwVIPy6EhT1YSOIhY5aF-G9nQSg-GK1V9LIq7petFoW_MIEt0yfNQVXy2D1tBhdJEa1bgtVsLmdlrNVf-m3uA" - ssl_ca_cert: "-----BEGIN CERTIFICATE----- + ssl_ca_cert: "-----BEGIN CERTIFICATE-----nID MIICwjCCAaqgAwIBAgIBADANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdrdWJl LWNhMB4XDTIwMDgyNjA5MzIzMVoXDTMwMDgyNDA5MzIzMVowEjEQMA4GA1UEAxMH a3ViZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxkeE16lPAd @@ -76,6 +76,66 @@ please refer to [#first]_. -----END CERTIFICATE-----" type: "kubernetes" +In addition to using ``bearer_token`` to authenticate with Kubernetes , +OpenID token [#sixth]_ is also supported. The following sample specifies +``oidc_token_url``, ``client_id``, ``client_secret``, ``username``, ``password`` +instead of ``bearer_token`` for OpenID token authentication. + +Before using OpenID token authentication, additional settings are required. +Please refer to [#seventh]_, and how to get the values of the ``oidc_token_url``, +``client_id``, ``client_secret``, ``username``, ``password`` and ``ssl_ca_cert`` +parameters is documented. + +The SSL certificates of Kubernetes and OpenID provider are concatenated +in ``ssl_ca_cert``. + +.. code-block:: console + + $ cat vim-k8s.yaml + auth_url: "https://192.168.33.100:6443" + project_name: "default" + oidc_token_url: "https://192.168.33.100:8443/realms/oidc/protocol/openid-connect/token" + client_id: "tacker" + client_secret: "A93HfOUpySm6BjPug9PJdJumjEGUJMhc" + username: "end-user" + password: "end-user" + ssl_ca_cert: "-----BEGIN CERTIFICATE----- + MIICwjCCAaqgAwIBAgIBADANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdrdWJl + LWNhMB4XDTIwMDgyNjA5MzIzMVoXDTMwMDgyNDA5MzIzMVowEjEQMA4GA1UEAxMH + a3ViZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxkeE16lPAd + pfJj5GJMvZJFcX/CD6EB/LUoKwGmqVoOUQPd3b/NGy+qm+3bO9EU73epUPsVaWk2 + Lr+Z1ua7u+iib/OMsfsSXMZ5OEPgd8ilrTGhXOH8jDkif9w1NtooJxYSRcHEwxVo + +aXdIJhqKdw16NVP/elS9KODFdRZDfQ6vU5oHSg3gO49kgv7CaxFdkF7QEHbchsJ + 0S1nWMPAlUhA5b8IAx0+ecPlMYUGyGQIQgjgtHgeawJebH3PWy32UqfPhkLPzxsy + TSxk6akiXJTg6mYelscuxPLSe9UqNvHRIUoad3VnkF3+0CJ1z0qvfWIrzX3w92/p + YsDBZiP6vi8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMB + Af8wDQYJKoZIhvcNAQELBQADggEBAIbv2ulEcQi019jKz4REy7ZyH8+ExIUBBuIz + InAkfxNNxV83GkdyA9amk+LDoF/IFLMltAMM4b033ZKO5RPrHoDKO+xCA0yegYqU + BViaUiEXIvi/CcDpT9uh2aNO8wX5T/B0WCLfWFyiK+rr9qcosFYxWSdU0kFeg+Ln + YAaeFY65ZWpCCyljGpr2Vv11MAq1Tws8rEs3rg601SdKhBmkgcTAcCzHWBXR1P8K + rfzd6h01HhIomWzM9xrP2/2KlYRvExDLpp9qwOdMSanrszPDuMs52okXgfWnEqlB + 2ZrqgOcTmyFzFh9h2dj1DJWvCvExybRmzWK1e8JMzTb40MEApyY= + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIC7TCCAdWgAwIBAgIUQK2k5uNvlRLx43LI/t3a2/A/3iQwDQYJKoZIhvcNAQEL + BQAwFTETMBEGA1UEAxMKa3ViZXJuZXRlczAeFw0yMjA4MDQwNjIwNTFaFw0yMzA4 + MDQwNjIwNTFaMBMxETAPBgNVBAMMCEtleWNsb2FrMIIBIjANBgkqhkiG9w0BAQEF + AAOCAQ8AMIIBCgKCAQEAni7HWLn2IpUImGO1sbBf/XuqATkXSeIIRuQuFymwYPoX + BP7RowzrbfF9KUwdIKlz9IXjqb1hplumiqNy1Sc7MmrTY9Fj87MNAMlnCIvyWkjE + XVXWxGef49mqc85P2K1iuAsr2R7sDrv7SC0ch+lHclOjGDmCjKOk8qF3kD1LATWg + zf42aXb4nNF9kyIOPEbI+jX4PWhAQpEz5nIG+xIRjTHGfacjpeg0+XOK21wLAuQB + fqebJ6GxX4OzB37ZtLLgrKyBYWaWuYkWbexVRM3wEvQu8ENkvhV017iPuPHSxNWx + Y8z072XMs9j8XRQD65EVqObXyizotPRJF4slEJ9qMQIDAQABozcwNTAJBgNVHRME + AjAAMAsGA1UdDwQEAwIF4DAbBgNVHREEFDAShwR/AAABhwTAqAIhhwQKCgCMMA0G + CSqGSIb3DQEBCwUAA4IBAQBebjmNHd8sJXjvPQc3uY/3KSDpk9AYfYzhUZvcvLNg + z0llFqXHaFlMqHTsz1tOH4Ns4PDKKoRT0JIKC1FkvjzqgL+X2jWFS0NRoNyd3W3B + yHLEL7MdQqDR+tZX02EGfaGXjuy8GHIU4J2hXhohmpn6ntfiRONfY8jaEjIecPFS + IwZWXNhsDESa1zuDe0PatES/Ati8bAUpN2rb/7rsE/AeM5GXpQfOKV0XxdIeBZ82 + Vf5cUDWPipvq2Q9KS+yrTvEObGtA6gKhQ4bpz3MieU3N8AtQpEKtROH7mJWMHyl2 + roD1k8KeJlfvR/XcVTGFcgIdNLfKIdd99Xfi4gSaIKuw + -----END CERTIFICATE-----" + type: "kubernetes" + 2. Register Kubernetes VIM ~~~~~~~~~~~~~~~~~~~~~~~~~~ We could register Kubernetes VIM to tacker by running the following command: @@ -857,3 +917,5 @@ References .. [#third] https://specs.openstack.org/openstack/tacker-specs/specs/victoria/container-network-function.html#kubernetes-resource-kind-support .. [#fourth] https://docs.openstack.org/tacker/latest/user/vnfd-sol001.html .. [#fifth] https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names +.. [#sixth] https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens +.. [#seventh] https://docs.openstack.org/tacker/latest/reference/kubernetes_openid_token_auth_usage_guide.html diff --git a/playbooks/devstack/pre.yaml b/playbooks/devstack/pre.yaml index 15043ad1a..cb8626bb8 100644 --- a/playbooks/devstack/pre.yaml +++ b/playbooks/devstack/pre.yaml @@ -3,6 +3,7 @@ - ensure-db-cli-installed - orchestrate-devstack - modify-heat-policy + - setup-k8s-oidc - setup-default-vim - setup-helm - role: setup-multi-tenant-vim diff --git a/releasenotes/notes/support-openid-k8s-vim-8767a454e6b0a72d.yaml b/releasenotes/notes/support-openid-k8s-vim-8767a454e6b0a72d.yaml new file mode 100644 index 000000000..0afba5f96 --- /dev/null +++ b/releasenotes/notes/support-openid-k8s-vim-8767a454e6b0a72d.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds "openid token" authentication strategy for Kubernetes VIM. Users can + specify openid authentication parameters in the `VIM Register Request` + or the `VNF Instantiate Request` to indicate tacker to use openid token + for authentication. diff --git a/roles/setup-default-vim/tasks/main.yaml b/roles/setup-default-vim/tasks/main.yaml index afadc7fbe..e24c2e5c5 100644 --- a/roles/setup-default-vim/tasks/main.yaml +++ b/roles/setup-default-vim/tasks/main.yaml @@ -97,7 +97,7 @@ become: yes become_user: stack - - name: Fetch k8s's CA Certificate + - name: Fetch k8s CA certificate fetch: src: "/etc/kubernetes/pki/ca.crt" dest: "/tmp/" @@ -105,6 +105,14 @@ when: - k8s_ssl_verify + - name: Fetch keycloak server certificate + fetch: + src: "/etc/keycloak/ssl/keycloak.crt" + dest: "/tmp/" + flat: true + when: + - keycloak_host is defined + when: - inventory_hostname == 'controller-k8s' - kuryr_k8s_api_url is defined @@ -123,6 +131,14 @@ src={{ devstack_base_dir }}/tacker/tacker/tests/etc/samples/local-k8s-vim.yaml dest={{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim.yaml + - name: Copy test k8s vim file for oidc + copy: + remote_src=True + src={{ devstack_base_dir }}/tacker/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml + dest={{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml + when: + - keycloak_host is defined + - name: Check if project's tools/test-setup-k8s-vim.sh exists stat: path: "{{ zuul_work_dir }}/tools/test-setup-k8s-vim.sh" @@ -169,6 +185,28 @@ when: - p.stat.exists + - name: Replace k8s auth uri in local-k8s-vim-oidc.yaml + replace: + path: "{{ item }}" + regexp: "https://127.0.0.1:6443" + replace: "{{ kuryr_k8s_api_url }}" + with_items: + - "{{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml" + when: + - p.stat.exists + - keycloak_host is defined + + - name: Replace keycloak uri in local-k8s-vim-oidc.yaml + replace: + path: "{{ item }}" + regexp: "https://127.0.0.1:8443" + replace: "https://{{ keycloak_host }}:{{ keycloak_https_port }}" + with_items: + - "{{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml" + when: + - p.stat.exists + - keycloak_host is defined + - name: Replace k8s auth token in local-k8s-vim.yaml replace: path: "{{ item }}" @@ -179,7 +217,7 @@ when: - p.stat.exists - - name: Copy k8s's CA Certificate to tacker + - name: Copy k8s CA certificate to tacker copy: src: "/tmp/ca.crt" dest: "/tmp/" @@ -187,23 +225,41 @@ - p.stat.exists - k8s_ssl_verify - - name: Get k8s's CA Certificate - command: cat "/tmp/ca.crt" - register: ssl_ca_cert + - name: Copy keycloak server certificate to tacker + copy: + src: "/tmp/keycloak.crt" + dest: "/tmp/" when: - p.stat.exists - - k8s_ssl_verify + - keycloak_host is defined - - name: Replace k8s CA Certificate in local-k8s-vim.yaml + - name: Write k8s CA certificate to a certificates aggregated file + shell: cat /tmp/ca.crt > /tmp/agg.crt + when: + - p.stat.exists + - k8s_ssl_verify + + - name: Write keycloak server certificate to a certificates aggregated file + shell: cat /tmp/keycloak.crt >> /tmp/agg.crt + when: + - p.stat.exists + - keycloak_host is defined + + - name: Register ssl certificate if exists + shell: test -f /tmp/agg.crt && cat /tmp/agg.crt + register: ssl_ca_cert + + - name: Replace ssl_ca_cert in local-k8s-vim.yaml and local-k8s-vim-oidc.yaml replace: path: "{{ item }}" regexp: "ssl_ca_cert: .*$" replace: "ssl_ca_cert: '{{ ssl_ca_cert.stdout }}'" with_items: - "{{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim.yaml" + - "{{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml" when: - p.stat.exists - - k8s_ssl_verify + - ssl_ca_cert.rc == 0 and ssl_ca_cert.stdout != "" - name: Replace the config file path in the test-setup-k8s-vim.sh replace: diff --git a/roles/setup-k8s-oidc/defaults/main.yaml b/roles/setup-k8s-oidc/defaults/main.yaml new file mode 100644 index 000000000..53d8b6321 --- /dev/null +++ b/roles/setup-k8s-oidc/defaults/main.yaml @@ -0,0 +1 @@ +oidc_work_dir: "/tmp/oidc" \ No newline at end of file diff --git a/roles/setup-k8s-oidc/files/cluster_role_binding.yaml b/roles/setup-k8s-oidc/files/cluster_role_binding.yaml new file mode 100644 index 000000000..b4df79e13 --- /dev/null +++ b/roles/setup-k8s-oidc/files/cluster_role_binding.yaml @@ -0,0 +1,11 @@ +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: oidc-cluster-admin-binding +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: User + name: end-user diff --git a/roles/setup-k8s-oidc/files/create_keycloak.sh b/roles/setup-k8s-oidc/files/create_keycloak.sh new file mode 100644 index 000000000..d354f53bc --- /dev/null +++ b/roles/setup-k8s-oidc/files/create_keycloak.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +docker run -d \ +--net=host \ +-e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \ +-e KC_HTTP_PORT=8080 -e KC_HTTPS_PORT=8443 \ +-e KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/keycloak.crt \ +-e KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/keycloak.key \ +-v /etc/keycloak/ssl:/opt/keycloak/conf quay.io/keycloak/keycloak:18.0.2 \ +start-dev diff --git a/roles/setup-k8s-oidc/files/generate_ssl_cert.sh b/roles/setup-k8s-oidc/files/generate_ssl_cert.sh new file mode 100644 index 000000000..519711a6b --- /dev/null +++ b/roles/setup-k8s-oidc/files/generate_ssl_cert.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +wk_dir=/tmp/oidc +req_conf=$wk_dir/ssl_csr.conf +ssl_dir=/etc/keycloak/ssl +key_file=$ssl_dir/keycloak.key +csr_file=$ssl_dir/keycloak.csr +crt_file=$ssl_dir/keycloak.crt + +k8s_ssl_dir=/etc/kubernetes/pki +k8s_ca_crt=$k8s_ssl_dir/ca.crt +k8s_ca_key=$k8s_ssl_dir/ca.key + +# make a directory for storing certificate +mkdir -p $ssl_dir + +# generate private key +openssl genrsa -out $key_file 2048 + +# generate certificate signing request +openssl req -new -key $key_file -out $csr_file -subj "/CN=Keycloak" -config $req_conf + +# use Kubernetes’s CA for issuing certificate +openssl x509 -req -in $csr_file -CA $k8s_ca_crt -CAkey $k8s_ca_key -CAcreateserial -out $crt_file -days 365 -extensions v3_req -extfile $req_conf + +# add executeable permission to key file +chmod 755 $key_file diff --git a/roles/setup-k8s-oidc/files/import_oidc_realm.sh b/roles/setup-k8s-oidc/files/import_oidc_realm.sh new file mode 100644 index 000000000..21d2f30f6 --- /dev/null +++ b/roles/setup-k8s-oidc/files/import_oidc_realm.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +KEYCLOAK_BASE_URL=https://127.0.0.1:8443 + +ADMIN_TOKEN=$(curl -k -sS -X POST "${KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token" \ +-H 'Content-Type: application/x-www-form-urlencoded' \ +-d 'username=admin' \ +-d 'password=admin' \ +-d 'grant_type=password' \ +-d 'client_id=admin-cli' | jq -r .access_token) +if [ $? -ne 0 ] +then + exit $? +fi + +curl -k -L -X POST "${KEYCLOAK_BASE_URL}/admin/realms" \ +-H 'Content-Type: application/json' \ +-H "Authorization: Bearer $ADMIN_TOKEN" \ +-d @"oidc_realm.json" \ No newline at end of file diff --git a/roles/setup-k8s-oidc/files/oidc_realm.json b/roles/setup-k8s-oidc/files/oidc_realm.json new file mode 100644 index 000000000..c84c08478 --- /dev/null +++ b/roles/setup-k8s-oidc/files/oidc_realm.json @@ -0,0 +1,1843 @@ +{ + "id" : "oidc", + "realm" : "oidc", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "d29e3b7c-bb0b-4878-9fc0-602221dc7c19", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "oidc", + "attributes" : { } + }, { + "id" : "c8004dc4-ece5-4248-b473-41e9ac256b1f", + "name" : "default-roles-oidc", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "manage-account", "view-profile" ] + } + }, + "clientRole" : false, + "containerId" : "oidc", + "attributes" : { } + }, { + "id" : "ba01101b-41db-4850-96b6-31e382bdf9ad", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "oidc", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "5e6915b5-2167-4284-97f6-1caef7957dbc", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "0e621226-a8d5-4e6d-be7c-cd7282c9ddfe", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "4f795fac-c90c-4b3d-bd23-fb69a070f834", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "61be3b06-aee2-4466-b26d-bb3ddd4a68f3", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "dcd9e428-b3dc-48f4-b36c-0bfef980df11", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "b6eda241-26ed-4ce8-a5d7-d715d46c36d0", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "0c384087-c4c7-47b5-981a-857d190bdd9b", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "06b9752e-d113-48df-a96c-afc548b6c14d", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "35d7539e-e019-455d-99a5-4c23448b50aa", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "b8bfd15b-aab9-4a2e-9898-92e9f955ace4", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "e4dfcc62-0c3e-4d50-9f66-ea3cb7df6e42", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "cbfe8a8a-f735-480c-ae5f-b194de999331", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "a7932c23-cde1-4cca-937a-4400ef8c02ab", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "51da0c35-11ca-4760-b39e-729fd0861e42", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "764e710b-633f-43f5-a81d-6069ada4b79a", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "b2208b7d-3379-4916-9d35-9cd374656dd3", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-realm", "manage-clients", "view-clients", "manage-realm", "query-users", "manage-identity-providers", "manage-authorization", "create-client", "query-realms", "query-groups", "view-events", "query-clients", "impersonation", "view-identity-providers", "view-users", "manage-events", "view-authorization", "manage-users" ] + } + }, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "64fadb8e-32fb-4d94-ba47-55b5b900fb13", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "daebd597-a7e0-49f0-b45f-b46128cc3502", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + }, { + "id" : "822d2377-a510-4461-b7fb-0c2adda29970", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "attributes" : { } + } ], + "tacker" : [ ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "aebe87b8-1527-4ceb-8449-a5fd7ab2aff9", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "8eeffcdb-a820-4421-bd2b-b5fcb4cc5c4e", + "attributes" : { } + } ], + "account" : [ { + "id" : "79c015b8-da1a-413c-b578-4f7b98563e0c", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "c6caf897-fcba-4ad7-a3d2-f2fcc434ca60", + "attributes" : { } + }, { + "id" : "9d276431-9f99-4728-8ef1-e9570d6252d4", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "c6caf897-fcba-4ad7-a3d2-f2fcc434ca60", + "attributes" : { } + }, { + "id" : "d633767b-3def-4ce4-ad15-629bbc26e973", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "c6caf897-fcba-4ad7-a3d2-f2fcc434ca60", + "attributes" : { } + }, { + "id" : "e14adaa7-ff7b-417b-bacd-52d4f909fb74", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "c6caf897-fcba-4ad7-a3d2-f2fcc434ca60", + "attributes" : { } + }, { + "id" : "29921186-d4e5-4811-a780-e0d0fa9c91ff", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "c6caf897-fcba-4ad7-a3d2-f2fcc434ca60", + "attributes" : { } + }, { + "id" : "6bcd592c-9afd-45df-aedf-ba6fc67310d3", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "c6caf897-fcba-4ad7-a3d2-f2fcc434ca60", + "attributes" : { } + }, { + "id" : "86e47647-8cd8-4835-bb17-ecd0934db4ed", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "c6caf897-fcba-4ad7-a3d2-f2fcc434ca60", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "c8004dc4-ece5-4248-b473-41e9ac256b1f", + "name" : "default-roles-oidc", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "oidc" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpSupportedApplications" : [ "FreeOTP", "Google Authenticator" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "users" : [ { + "id" : "96e52328-6129-47d0-9989-6edbad9428ea", + "createdTimestamp" : 1662446980799, + "username" : "end-user", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "attributes" : { + "name" : [ "end-user" ] + }, + "credentials" : [ { + "id" : "2c2c0639-f9f9-45d9-98e7-aa33bf100776", + "type" : "password", + "createdDate" : 1662447010297, + "secretData" : "{\"value\":\"lDs6tZFb4mn65os+ULX1TUCtm3DIwK2HOl6iKG1MsEblVSfKOnHFBesdCKs8cbusOZAdy74DtvU66h1vP/wWDA==\",\"salt\":\"tpJLit4pFSdQshHBdZVpBA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-oidc" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account" ] + } ] + }, + "clients" : [ { + "id" : "c6caf897-fcba-4ad7-a3d2-f2fcc434ca60", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/oidc/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/oidc/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "a65c4a54-9002-43a9-a9c1-8c766d79d691", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/oidc/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/oidc/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "16a817a5-14af-4608-98b5-cadc70cd0f18", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "24e0d76a-e390-4beb-9dca-59f20bcd86b9", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "8eeffcdb-a820-4421-bd2b-b5fcb4cc5c4e", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "0fb05364-7501-4828-9405-fc18bc7dba2d", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "4b902faa-aaf2-44c9-822d-ca91ac319897", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/oidc/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/oidc/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "a7420c26-961d-4db6-b120-59bc6a55362c", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "7976e190-ad99-464f-b80e-0029f88be0da", + "clientId" : "tacker", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "K0Zp5dvdOFhZ7W9PVNZn14omW9NmCQvQ", + "redirectUris" : [ "http://*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "saml.force.post.binding" : "false", + "saml.multivalued.roles" : "false", + "frontchannel.logout.session.required" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "saml.server.signature.keyinfo.ext" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "require.pushed.authorization.requests" : "false", + "saml.client.signature" : "false", + "saml.allow.ecp.flow" : "false", + "id.token.as.detached.signature" : "false", + "saml.assertion.signature" : "false", + "client.secret.creation.time" : "1662447097", + "saml.encrypt" : "false", + "saml.server.signature" : "false", + "exclude.session.state.from.auth.response" : "false", + "saml.artifact.binding" : "false", + "saml_force_name_id_format" : "false", + "acr.loa.map" : "{}", + "tls.client.certificate.bound.access.tokens" : "false", + "saml.authnstatement" : "false", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false", + "saml.onetimeuse.condition" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "13c47a7d-4671-44cd-bc94-c9635b13601d", + "name" : "name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "name", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "name", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "613f58b0-b75e-4a0e-aaf6-fbb9c84a1781", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "3744f78b-d435-4cc2-8367-812e24e5f303", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "cd60e6fc-1bb8-4b89-9220-6f61a0747524", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "3101dd36-3aa8-47d5-ab02-afd164203fb6", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "84e5f07a-b077-41ae-bbbd-39b0c850fa72", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "0cbfa1c1-0d77-40c2-90dd-319645096929", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "255afd80-2297-4bf3-9d60-c23b96e44faf", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "a54c4375-d74c-4138-9f34-2fd3ce79e844", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "fe521190-c217-4cbf-b893-a2fcc8233553", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "afd049e0-f000-4bea-b9e1-586866a2a192", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "2053c06f-574c-4c67-a2f8-0b927afc47a8", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "0d33e9f7-e9bd-46f2-ade7-982fb56aa4ed", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "ab71478b-b068-491b-8414-0536b02dcd8a", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "5c7c998e-bfb9-4174-aa18-c366d3d79c9f", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "27aabf78-618f-4b24-b347-4000c16bc940", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "d5d0731c-f14a-4de1-8765-131528592367", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "e0faf26d-9c2a-4524-b8aa-791c30d778a6", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "99e5c422-ed3e-4541-b554-4e8747358728", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "75320c52-4fbb-43e1-9cab-e81a1fa44f69", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "f9651ec3-129f-42c5-b613-8428780f5a2e", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "d8daf4a8-cac4-41ce-b5df-7c3f39b703e5", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "4e205b98-4667-4bd7-abe7-c919992ed513", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + } ] + }, { + "id" : "b87f5364-2bf6-497b-975b-b629fb84f753", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "b1b8dd77-0736-4047-b8c9-98b8f669941e", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "cb7c8bb2-9901-4270-8e77-dc5c59013d7c", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "c34cee8d-5724-4c4f-982f-a1f6759d563d", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "64a9f5a1-f16b-4cc3-9a33-d97ec1820365", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "e6fe5c04-ec0c-451d-9e94-ce99ee2692f4", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "c9e4b0b8-07c7-424d-acea-5af1bcff5184", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "6192638b-1fc1-4160-8529-8b10a68c7106", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "012e6d8c-1d8b-4eee-943c-5dbdf7f65361", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "bbb28710-a0bc-4e25-9f58-46c6eaf4a6e0", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "5c375c89-f600-4ba5-908b-aaa1d59992f1", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "76567612-76fa-4536-94a3-f2a396e958a5", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "617c9eb5-3e4e-4356-b5da-d8b453fdaf20", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "4f74badb-98eb-48db-939e-6d7dd69fc2b4", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "50cac1e7-6729-435b-afc8-44d2c3f1e777", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "dde8f327-34bc-4b0c-8bf5-9e5e7cda5b12", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "9589a161-8463-4a2f-b742-ac602c718aed", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "6342164d-80d9-4214-bfb3-d9eed9492b54", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "498a1139-819a-49f5-a5ac-7616334159cf", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper" ] + } + }, { + "id" : "691f62c7-b5ad-45bb-b0d7-36331e3eaf93", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "c21e6d44-f52a-4ae6-9d54-66324f73ef7e", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "2ad8a6ce-4701-4b64-b518-b13a9e401575", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-address-mapper", "oidc-full-name-mapper" ] + } + }, { + "id" : "3835a80f-ed98-494d-9ecb-b6e4c112ed57", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "d44e360c-5d09-4833-a544-fe0a22bf362a", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEAnpeqE8e0uJcG507iW3bfxWG3oKxKm2PBNeOB3vkkpVeGh+Jaa1T9t1m9USNMcKTRtSsfB3S8QzNOEuAkLMKxNHnBScu6kL8lduZ4aSJeB+AwtuxpD7yq5RjdDShf+zWRlBJmrD/aN4gvrOuUc4jV3WtrCDberZsb8gpmwY55T1EzJ5tYNkFVKnoIf27iZbnZ0bzeIKc2m7eQrI+bKxju8MholOP9a0WGIXqUVfXWfMb4jWv9gjVHJJNY9Th0U0kEhtDD0yBksDqgvgCX224lN/GEZEFXOAmccd91YxnA2D4gPWvWAGOzyR/plgFLAxWjjWPneI6TUzCwfSZWyGcEKQIDAQABAoIBAD3kwOqxUPWNc5NIdQzvKsvUpB6fvwo/90K5xJCpuUSSER3tGA61f8NT0Y/d79IycOl8p8J4K5Uqa7jwIw4Y+aGaNvY+/VPKVau+rJWTZRE7zKdHQoVyw1tfxb2B2Vun/2XDfLCSxu5PRxP8aNZiO90LNB1jlKo6U8C1kH7PxMZQM1p2sRVuPBsTN0eLchcHSADt51ZxmAmsyzeKP6PuoIsEcj3IkYwiJ/AuIrvq2+xmygm5vQlVGfXWlCYZ0kwd6SuHvBrpaEEeEaoX8a/Lpv8X3dqR5fBXV63QTyJjeZHPtqAjW8lNMsdLblDXOfGFBMy9fx14YCLKKsbJVRK+DqUCgYEA3hUNG6XuGdcQ3t0kgS6pVVXGGsPyJOqqSUiJsHnuroXxCoMncvTu8cFQ/D7E5jruUnfiT64gfO32ztuFf65CYgXE/8h0DL9FXA5GHtanpFLh+pX6wfeYnaDTUausM/ERBxKgvqDyD4r2p8SjLNfLGjkP0ic3zWXTfx/bJ6QR/9sCgYEAttBLrmoym2i3er08zRt3TK6XH+G8zR4IDiVctoNr2IcOhkDKaPcTqzPpd6QSYfTuF/incrmBhlAb521ssEOJR393Knoto1VvY+SR3lKAeQAOava0W0b5dTD+Zc8VRYGKwhIFbzmj3atjH3v5utUYFoI3eqELsIvdCDKqD5AT3UsCgYEAktxi9bSuFyJ1ApxFRrRfwJHfVtXbbHROtfWlMDICGCF1PBltXgUBepf3gUfVF9dCwQCMhVrGGzeWbkcXKk9HkOD13JxnugJG0NCTqFMVO4Kf9AF4eQrOPvcap7iaQSMauo2kBUwTpxmjcWCE8+OkaSvw/W135nl++mNLnxRN3t8CgYEAkt5W3uGslJQVS8M6VKGrP2zINrHZR4TH/e1gRbThcIxYS91Df/53y8Qh3Z9vsUjf+1wl0pJcD7bOJCgR+K3ZXRp3dyW/AoiBu+QGmHD5i7xS2PYoQWiMwuzAhLRQp42CF5X4zbml/1FQihvErqfB+VtWDOvTA1vqEEr7uxMKEm8CgYBOOstm0aiVXi72mdIya0LWmSe1Z5OeB6UfBG/vIfS0A++v+RI72TgIvLZ2xukkAMuieAm0injMmTIunbIo+g29jmZ0ExkwhsXrrvZGmhUjVUGHySQI6YbZBPTEgXdGPwijpwRAzIEYhoqe+ZJyKCbXpjWA8o3S6VvfF+b2AYOUkw==" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIIClzCCAX8CBgGDEY5znzANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARvaWRjMB4XDTIyMDkwNjA2NDYzMVoXDTMyMDkwNjA2NDgxMVowDzENMAsGA1UEAwwEb2lkYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ6XqhPHtLiXBudO4lt238Vht6CsSptjwTXjgd75JKVXhofiWmtU/bdZvVEjTHCk0bUrHwd0vEMzThLgJCzCsTR5wUnLupC/JXbmeGkiXgfgMLbsaQ+8quUY3Q0oX/s1kZQSZqw/2jeIL6zrlHOI1d1rawg23q2bG/IKZsGOeU9RMyebWDZBVSp6CH9u4mW52dG83iCnNpu3kKyPmysY7vDIaJTj/WtFhiF6lFX11nzG+I1r/YI1RySTWPU4dFNJBIbQw9MgZLA6oL4Al9tuJTfxhGRBVzgJnHHfdWMZwNg+ID1r1gBjs8kf6ZYBSwMVo41j53iOk1MwsH0mVshnBCkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJJ7fyInTpC00EbpH33On4B1iZvqWlkjc6Z5o4rKjMmd2U34F3RXwXEJ1m6uLwrudIIczJuVXKVmYQ0bzzBGITdA7VK2FU5MfjZbZwJCykBbIMkMo+w6mmelqSMgORunaQpFl478CJjwqt3VsXRYRmdB/Tydla2IynhTtquzKWqlDLaMsMi8DYTsJfwY+2Ua4AeTML4pax41nhh6vUoWGGKv1M3UScHjMcC0v/6Tyc443qGPtgNwshjHdmMIED2y/UGNodLcwf0MRrPihqqVh/slks7b7Q1/eqKjhxkhsFkbHxdTsCKLBVriZmCw6+Ls5z5KHPa3K7x9smI6EyHrxKg==" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "d029e8af-e741-4b98-b64e-9772c2f6abe1", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "9da0d8b6-d184-449d-a841-c13a510b73e5" ], + "secret" : [ "tsenbqS3xhomjaeBEDRHVQ" ], + "priority" : [ "100" ] + } + }, { + "id" : "f96a3b11-292e-457d-9391-2e5d8e1365a4", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAk8OcXrdN/6V8o/fxLO5AHqq7OQYEvU3TUZwcu3sSh6BhFUs4BVx4YkxKwT6Q9b7EtE5pp1TK5w8FQnaJTISRwoAhnTNyytrVgcqmtmwkbSLyZMu6Q0+hJOdSb+dRcNnDckEfV1NJxE07dKXnzD9l+wla1vkPXYIZRMirOhJWW8uFBVGH79dVxukbYnKPZ81dzz4ruwusZ3zXya1iZtHp8VZZgiddltMICKY6loVLBL7sPa40/nHhYfjgcxmLD7XLkZidT4Ob0yiMLSbuou2WVBJwoEp6KUiFeFt1ui59mB4fHEdEvW4ZCni5/n8e8uhIG/2LAzILrAOyKw1Pw2ffBwIDAQABAoIBAFiIIM5XAXKkUjNjwKps7RnyrU4THw/U60HASS7DJG0CWGiLsrrzlmU+1KNHu06hx/VH7eGF2jPqOfqCTuz7mOFyJ7GqO6Lyzm1/X7d4v7Jr99MQhT8DHMehmTDW5WK59nIkNoT5r/7fTzoqlOE5Mv7fsf+PJfeelUky+fRHPFqugqq8vzy6YfBbZgGvJg8cVrlk1vz6pOwN4vGhPmD8Awo9wCT6W04lgnJUfnqDYc5eoDw0nRQ2nWUjkLsFb3ZvEznK9zcrpAzVRAntL+7TtRu8ot9WoN4Rt2SCfZ4QW4hds3XzryEFi4eYf1NBZAGba3cEySoTGDfuf3h25iHskGECgYEAwqc2A5kFrBnISdLv9oFUk3GEieyJkA7qNaNoRTJOOMM7n2DSMk7LyENl0TU5U9sSrVasIaSZGhNpQo6eHPLH/tivlLrStQwGZ978AB/wBsPDhGFtvj8s2+UI0MVvp3Tun4MZBzQp+GA8JmwtCMVNPCSQZ7/ucqSt5cABc+GVZikCgYEAwlVZUsEUF/HPc4BTLNRA3bsuMAb1KCeu7A8pxUr6gXsVRi4lo5q+f8gF2NVLhSLPtWXZPcMnaGv1Uu0I6oWDbXOk+ZkKYczgBuuLMEFubdxeNsPhDHdv2PIXz3KJFARg9R+iVcIw8Xfq+9XOp9VVYkvx+wtFkJXm4Ji/HnE14a8CgYEAi2+AL7+T4p5tbQSfRHOMhDoS+UHpoLouZ9HwRXEtu1ePBDI1IDh1nbU54E1CDaGmlWi60Ta5PYaeJkFHXfFD9nh9/gp+GZbNl+aRmf1G0XG2QsQP+vICTlqYoARuYQRQUw90BEcHsZYuQE/JSrkbBHQkxU0loBX7Q9Lwt3Pms1ECgYBvim1qgkf2NmTL9qvG300b94PbLSMpmR1dgJaJFzARgYQEdBhGf1s4HKa+fi6KjCCMDZlTSeAkn1J/9m1Xrqpw+B+I476BxAYR8sBBQk0P4Zxx8pwJe8RG0S25dBQZ2SmNiEq0znEpJ5tIUL/8tQX9FXoejamwA1oxL3sDDhfPIQKBgBvf8PumyvBOMBN2obwdQLInbANvpaXam7EzHZDrqIHLNho6CTkUq6tYfHPh8nj2JwotdX7K0zbooOPrf32L8oyRUhI5MAQCwPoZsJ9j9Yvv6M78jFxKbUboKte27LYz6GPZwQITRwJprCDTSS+Z3G0TmBi2k42mnduLPAJMKXdN" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIIClzCCAX8CBgGDEY5yLTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARvaWRjMB4XDTIyMDkwNjA2NDYzMVoXDTMyMDkwNjA2NDgxMVowDzENMAsGA1UEAwwEb2lkYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJPDnF63Tf+lfKP38SzuQB6quzkGBL1N01GcHLt7EoegYRVLOAVceGJMSsE+kPW+xLROaadUyucPBUJ2iUyEkcKAIZ0zcsra1YHKprZsJG0i8mTLukNPoSTnUm/nUXDZw3JBH1dTScRNO3Sl58w/ZfsJWtb5D12CGUTIqzoSVlvLhQVRh+/XVcbpG2Jyj2fNXc8+K7sLrGd818mtYmbR6fFWWYInXZbTCAimOpaFSwS+7D2uNP5x4WH44HMZiw+1y5GYnU+Dm9MojC0m7qLtllQScKBKeilIhXhbdboufZgeHxxHRL1uGQp4uf5/HvLoSBv9iwMyC6wDsisNT8Nn3wcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQqon8sKCqFbdz1mAbn8jBU8AIo1zcjInNfqBdIEaRaYebKiaM8U4uyMc9zlSCph0diRiy2kTbwx2jqk78cEFFcogRvGrONfZz7n5WllXk7LdFLG+sNnSNC1kgNAu/f2pxwDQHABgpjJENi38Bpo7jNX+GBcMjUgq9dc28DBV/0QtEm6THRnMrHapvxrJ6ujEGMTYjVQoqrsIX8mOj7w6NknNcoFxPiQHM+8igCDG4EdvUlU2lH/DODF0rU21qui1BEyP3SsQOEEwenVkDcnz/cF1LIlhnRgJcB2uQ4uczpBnzc9wZl7etHIq7ZprpzlMZe7wqcAvQqZocnjnPezklQ==" ], + "priority" : [ "100" ] + } + }, { + "id" : "5270527a-3a76-4733-8484-8072275bc9f1", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "5d3595cf-a8fa-4904-920a-80ef9e7070c9" ], + "secret" : [ "_Lsq-8Htjc1gM2EPGNlnEhQQ7dahWyCt6UHTdHd9-81CKi1Qmd6DVRdSlHOMCP9BY60tF4h-pkQnhep3tIB2kw" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "dd4c0cf7-bba1-4b28-a52a-9fbf62331b1f", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "f2d6db36-c002-4c98-b936-7e782544e753", + "alias" : "Authentication Options", + "description" : "Authentication options.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "basic-auth", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "basic-auth-otp", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "386af9a8-ccfb-4b85-8142-14013e9dbe69", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "572bb2df-e605-46bc-8672-734c36406715", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "ca5ccff0-4d78-4210-9189-765988fad50b", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "16f3b4b1-9a6e-4567-a605-0cac26135a4c", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "b483c8c5-4884-493e-8eb8-b37e78240771", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "04f498e8-2f60-412a-8a9f-43b21c4b0932", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "ac2a493f-44ca-4a2f-aeea-d6b76da8994f", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "ab9fedb1-63d3-40e0-9bb4-e44dedfbeaa0", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "136626ef-9299-405a-b938-21cb4c68cc84", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "0d771a94-dce6-4586-9127-d30ac7c3b3e7", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "54b2b3ac-12d3-4d7e-83f9-45908b906a73", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "ecb2ebf6-e1f2-45ca-a15d-ca49da49c1f7", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "465247d9-f374-453d-9a56-38401a189581", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "d744e6a5-29d4-4ed9-bd23-2401429cc853", + "alias" : "http challenge", + "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "no-cookie-redirect", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Authentication Options", + "userSetupAllowed" : false + } ] + }, { + "id" : "ff523ac3-317c-4c6a-b0dc-45726794bdd9", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "e9bfc331-94ab-48d7-8bb3-e9e863370148", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "a867e4f3-9b66-41d1-a10a-775d74495308", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "2ef0a8c7-d133-4f94-86a8-15df8a747958", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "8ba7ec2c-298c-47ad-9613-b5f9230b3361", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "1b396a87-bca7-4cbc-9f91-69f8b7e44eca", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "terms_and_conditions", + "name" : "Terms and Conditions", + "providerId" : "terms_and_conditions", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "oauth2DevicePollingInterval" : "5", + "parRequestUriLifespan" : "60", + "cibaInterval" : "5" + }, + "keycloakVersion" : "18.0.2", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} \ No newline at end of file diff --git a/roles/setup-k8s-oidc/files/ssl_csr.conf b/roles/setup-k8s-oidc/files/ssl_csr.conf new file mode 100644 index 000000000..894d269cb --- /dev/null +++ b/roles/setup-k8s-oidc/files/ssl_csr.conf @@ -0,0 +1,12 @@ +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name +[req_distinguished_name] + +[ v3_req ] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +subjectAltName = @alt_names + +[alt_names] +IP.1 = 127.0.0.1 \ No newline at end of file diff --git a/roles/setup-k8s-oidc/tasks/main.yaml b/roles/setup-k8s-oidc/tasks/main.yaml new file mode 100644 index 000000000..1feda8bca --- /dev/null +++ b/roles/setup-k8s-oidc/tasks/main.yaml @@ -0,0 +1,109 @@ +- block: + - name: Create a work directory + file: + path: "{{ oidc_work_dir }}" + state: directory + mode: 0755 + become: yes + + - name: Copy setup files to k8s server + copy: + src: "{{ item }}" + dest: "{{ oidc_work_dir }}" + owner: stack + group: stack + mode: 0644 + with_items: + - "ssl_csr.conf" + - "generate_ssl_cert.sh" + - "create_keycloak.sh" + - "oidc_realm.json" + - "import_oidc_realm.sh" + - "cluster_role_binding.yaml" + become: yes + + - name: Replace {{ oidc_work_dir }} if not default + replace: + path: "{{ item }}" + regexp: "/tmp/oidc" + replace: "{{ oidc_work_dir }}" + with_items: + - "{{ oidc_work_dir }}/generate_ssl_cert.sh" + when: + - oidc_work_dir != '/tmp/oidc' + become: yes + + - name: Add keycloak's ip to CSR conf + lineinfile: + path: "{{ oidc_work_dir }}/ssl_csr.conf" + line: "IP.2 = {{ keycloak_host }}" + become: yes + + - name: Generate SSL certificate for keycloak + command: /bin/bash {{ oidc_work_dir }}/generate_ssl_cert.sh + become: yes + + - name: Create and start keycloak server + command: /bin/bash {{ oidc_work_dir }}/create_keycloak.sh + become: yes + + - name: Wait for keycloak be active + wait_for: + host: "{{ keycloak_host }}" + port: "{{ keycloak_https_port }}" + delay: 120 + timeout: 300 + + - name: Install jq command + package: + name: jq + state: present + become: yes + + - name: Replace keycloak host:port to import_oidc_realm.sh + replace: + path: "{{ item }}" + regexp: "https://127.0.0.1:8443" + replace: "https://{{ keycloak_host}}:{{ keycloak_https_port }}" + with_items: + - "{{ oidc_work_dir }}/import_oidc_realm.sh" + become: yes + + - name: Import oidc realm + command: /bin/bash import_oidc_realm.sh + args: + chdir: "{{ oidc_work_dir }}" + become: yes + + - name: Setup oidc on k8s server + blockinfile: + path: /etc/kubernetes/manifests/kube-apiserver.yaml + insertafter: "- --tls-private-key-file=.*" + block: |2 + - --oidc-issuer-url=https://{{ keycloak_host }}:{{ keycloak_https_port }}/realms/oidc + - --oidc-client-id=tacker + - --oidc-username-claim=name + - --oidc-username-prefix=- + - --oidc-ca-file=/etc/kubernetes/pki/ca.crt + become: yes + ignore_errors: yes + + - name: Wait for k8s apiserver to restart + wait_for: + host: "{{ hostvars['controller-k8s']['nodepool']['private_ipv4'] }}" + port: 6443 + delay: 30 + timeout: 180 + ignore_errors: yes + + - name: Create clusterrolebinding on k8s server + command: > + kubectl create -f {{ oidc_work_dir }}/cluster_role_binding.yaml + become: yes + become_user: stack + ignore_errors: yes + + when: + - inventory_hostname == 'controller-k8s' + - keycloak_host is defined + diff --git a/tacker/common/container/kubernetes_utils.py b/tacker/common/container/kubernetes_utils.py index 385de5e68..8ca316f64 100644 --- a/tacker/common/container/kubernetes_utils.py +++ b/tacker/common/container/kubernetes_utils.py @@ -23,6 +23,8 @@ from kubernetes.client import api_client from oslo_config import cfg from oslo_log import log as logging +from tacker.common import oidc_utils + LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -32,15 +34,33 @@ class KubernetesHTTPAPI(object): def get_k8s_client(self, auth_plugin): config = client.Configuration() config.host = auth_plugin['auth_url'] - if ('username' in auth_plugin) and ('password' in auth_plugin)\ - and (auth_plugin['password'] is not None): - config.username = auth_plugin['username'] - config.password = auth_plugin['password'] - basic_token = config.get_basic_auth_token() - config.api_key['authorization'] = basic_token - if 'bearer_token' in auth_plugin: + if 'oidc_token_url' in auth_plugin: + # obtain token from oidc server + if 'id_token' not in auth_plugin: + id_token = oidc_utils.get_id_token_with_password_grant( + auth_plugin.get('oidc_token_url'), + auth_plugin.get('username'), + auth_plugin.get('password'), + auth_plugin.get('client_id'), + client_secret=auth_plugin.get('client_secret'), + ssl_ca_cert=auth_plugin.get('ca_cert_file') + ) + auth_plugin['id_token'] = id_token + + # set id token to k8s config config.api_key_prefix['authorization'] = 'Bearer' - config.api_key['authorization'] = auth_plugin['bearer_token'] + config.api_key['authorization'] = auth_plugin['id_token'] + + else: + if ('username' in auth_plugin) and ('password' in auth_plugin)\ + and (auth_plugin['password'] is not None): + config.username = auth_plugin['username'] + config.password = auth_plugin['password'] + basic_token = config.get_basic_auth_token() + config.api_key['authorization'] = basic_token + if 'bearer_token' in auth_plugin: + config.api_key_prefix['authorization'] = 'Bearer' + config.api_key['authorization'] = auth_plugin['bearer_token'] ca_cert_file = auth_plugin.get('ca_cert_file') if ca_cert_file is not None: config.ssl_ca_cert = ca_cert_file diff --git a/tacker/common/oidc_utils.py b/tacker/common/oidc_utils.py new file mode 100644 index 000000000..a192c8bf5 --- /dev/null +++ b/tacker/common/oidc_utils.py @@ -0,0 +1,57 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +"""Utilities functions for get openid token.""" + +import requests + +from tacker.extensions.vnfm import OIDCAuthFailed + + +def get_id_token_with_password_grant( + token_endpoint, username, password, client_id, + client_secret=None, ssl_ca_cert=None, timeout=20): + + if not token_endpoint or not username or not password or not client_id: + raise OIDCAuthFailed(detail='token_endpoint, username, password,' + ' client_id can not be empty.') + + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + body = { + 'grant_type': 'password', + 'scope': 'openid', + 'client_id': client_id, + 'client_secret': client_secret, + 'username': username, + 'password': password + } + + verify = ssl_ca_cert if ssl_ca_cert else False + + try: + resp = requests.post(token_endpoint, headers=headers, data=body, + verify=verify, timeout=timeout) + + if (resp.status_code == 200 + and resp.headers['Content-Type'] == 'application/json'): + return resp.json()['id_token'] + + raise OIDCAuthFailed( + detail=f'response code: {resp.status_code}, body: {resp.text}') + except requests.exceptions.RequestException as exc: + raise OIDCAuthFailed(detail=str(exc)) diff --git a/tacker/db/nfvo/nfvo_db.py b/tacker/db/nfvo/nfvo_db.py index 6b28bb87f..c9fd521f0 100644 --- a/tacker/db/nfvo/nfvo_db.py +++ b/tacker/db/nfvo/nfvo_db.py @@ -52,7 +52,10 @@ class Vim(model_base.BASE, class VimAuth(model_base.BASE, models_v1.HasId): vim_id = sa.Column(types.Uuid, sa.ForeignKey('vims.id'), nullable=False) - password = sa.Column(sa.String(255), nullable=False) + # NOTE(Yao Qibin): The password is nullable in the actual database, and + # password is not necessary in some cases(eg. use bearer token for auth), + # so change the nullable from False to True. + password = sa.Column(sa.String(255), nullable=True) auth_url = sa.Column(sa.String(255), nullable=False) vim_project = sa.Column(types.Json, nullable=False) auth_cred = sa.Column(types.Json, nullable=False) diff --git a/tacker/db/nfvo/nfvo_db_plugin.py b/tacker/db/nfvo/nfvo_db_plugin.py index edc9b51c0..981a0ea80 100644 --- a/tacker/db/nfvo/nfvo_db_plugin.py +++ b/tacker/db/nfvo/nfvo_db_plugin.py @@ -52,9 +52,20 @@ class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin): res['auth_url'] = vim_auth_db[0].auth_url res['vim_project'] = vim_auth_db[0].vim_project res['auth_cred'] = vim_auth_db[0].auth_cred - res['auth_cred']['password'] = vim_auth_db[0].password + if vim_auth_db[0].password: + res['auth_cred']['password'] = vim_auth_db[0].password + # NOTE(Yao Qibin): Since oidc_token_url contains keyword `token`, + # its value will be masked. + # To prevent its value from being masked, temporarily change its name. + if "oidc_token_url" in res['auth_cred']: + res['auth_cred']['oidc_x_url'] = res['auth_cred'].pop( + 'oidc_token_url') if mask_password: res['auth_cred'] = strutils.mask_dict_password(res['auth_cred']) + # Revert to oidc_token_url + if "oidc_x_url" in res['auth_cred']: + res['auth_cred']['oidc_token_url'] = res['auth_cred'].pop( + 'oidc_x_url') return self._fields(res, fields) def _fields(self, resource, fields): @@ -93,7 +104,7 @@ class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin): vim_auth_db = nfvo_db.VimAuth( id=uuidutils.generate_uuid(), vim_id=vim.get('id'), - password=vim_cred.pop('password'), + password=vim_cred.pop('password', None), vim_project=vim.get('vim_project'), auth_url=vim.get('auth_url'), auth_cred=vim_cred) @@ -167,7 +178,7 @@ class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin): except orm_exc.NoResultFound: raise nfvo.VimNotFoundException(vim_id=vim_id) vim_auth_db.update({'auth_cred': vim_cred, 'password': - vim_cred.pop('password'), 'vim_project': + vim_cred.pop('password', None), 'vim_project': vim_project}) vim_db.update({'updated_at': timeutils.utcnow()}) self._cos_db_plg.create_event( diff --git a/tacker/extensions/vnfm.py b/tacker/extensions/vnfm.py index 039e29110..7606df750 100644 --- a/tacker/extensions/vnfm.py +++ b/tacker/extensions/vnfm.py @@ -257,6 +257,11 @@ class InvalidMaintenanceParameter(exceptions.InvalidInput): message = _("Could not find the required params for maintenance") +class OIDCAuthFailed(exceptions.InvalidInput): + message = _("OIDC authentication and authorization failed." + " Detail: %(detail)s") + + def _validate_service_type_list(data, valid_values=None): if not isinstance(data, list): msg = _("Invalid data format for service list: '%s'") % data diff --git a/tacker/nfvo/drivers/vim/kubernetes_driver.py b/tacker/nfvo/drivers/vim/kubernetes_driver.py index 675fbe722..bf8e396e3 100644 --- a/tacker/nfvo/drivers/vim/kubernetes_driver.py +++ b/tacker/nfvo/drivers/vim/kubernetes_driver.py @@ -139,6 +139,10 @@ class Kubernetes_Driver(abstract_vim_driver.VimAbstractDriver): self.discover_placement_attr(vim_obj) self.encode_vim_auth(vim_obj['id'], vim_obj['auth_cred']) + # NOTE(Yao Qibin): To avoid obtaining token multiple times, + # the id_token is keeped in auth_cred, which will be deleted here. + if 'id_token' in vim_obj['auth_cred']: + vim_obj['auth_cred'].pop('id_token') LOG.debug('VIM registration completed for %s', vim_obj) @log.log @@ -197,6 +201,10 @@ class Kubernetes_Driver(abstract_vim_driver.VimAbstractDriver): encoded_auth = fernet_obj.encrypt( auth['ssl_ca_cert'].encode('utf-8')) auth['ssl_ca_cert'] = encoded_auth + if 'client_secret' in auth: + encoded_auth = fernet_obj.encrypt( + auth["client_secret"].encode("utf-8")) + auth["client_secret"] = encoded_auth if CONF.k8s_vim.use_barbican: try: diff --git a/tacker/nfvo/nfvo_plugin.py b/tacker/nfvo/nfvo_plugin.py index cc68dc5e4..ce666d836 100644 --- a/tacker/nfvo/nfvo_plugin.py +++ b/tacker/nfvo/nfvo_plugin.py @@ -161,7 +161,21 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, # is not updatable. so no need to consider it if 'auth_cred' in update_args: auth_cred = update_args['auth_cred'] - if ('username' in auth_cred) and ('password' in auth_cred)\ + if 'oidc_token_url' in auth_cred: + # update oidc info, and remove bearer_token if exists + vim_obj['auth_cred']['oidc_token_url'] = auth_cred.get( + 'oidc_token_url') + vim_obj['auth_cred']['username'] = auth_cred.get( + 'username') + vim_obj['auth_cred']['password'] = auth_cred.get( + 'password') + vim_obj['auth_cred']['client_id'] = auth_cred.get( + 'client_id') + vim_obj['auth_cred']['client_secret'] = auth_cred.get( + 'client_secret') + if 'bearer_token' in vim_obj['auth_cred']: + vim_obj['auth_cred'].pop('bearer_token') + elif ('username' in auth_cred) and ('password' in auth_cred)\ and (auth_cred['password'] is not None): # update new username and password, remove bearer_token # if it exists in the old vim @@ -169,15 +183,27 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, vim_obj['auth_cred']['password'] = auth_cred['password'] if 'bearer_token' in vim_obj['auth_cred']: vim_obj['auth_cred'].pop('bearer_token') + if 'oidc_token_url' in vim_obj['auth_cred']: + vim_obj['auth_cred'].pop('oidc_token_url') + if 'client_id' in vim_obj['auth_cred']: + vim_obj['auth_cred'].pop('client_id') + if 'client_secret' in vim_obj['auth_cred']: + vim_obj['auth_cred'].pop('client_secret') elif 'bearer_token' in auth_cred: # update bearer_token, remove username and password # if they exist in the old vim vim_obj['auth_cred']['bearer_token'] =\ auth_cred['bearer_token'] - if ('username' in vim_obj['auth_cred']) and\ - ('password' in vim_obj['auth_cred']): + if 'username' in vim_obj['auth_cred']: vim_obj['auth_cred'].pop('username') + if 'password' in vim_obj['auth_cred']: vim_obj['auth_cred'].pop('password') + if 'oidc_token_url' in vim_obj['auth_cred']: + vim_obj['auth_cred'].pop('oidc_token_url') + if 'client_id' in vim_obj['auth_cred']: + vim_obj['auth_cred'].pop('client_id') + if 'client_secret' in vim_obj['auth_cred']: + vim_obj['auth_cred'].pop('client_secret') if 'ssl_ca_cert' in auth_cred: # update new ssl_ca_cert vim_obj['auth_cred']['ssl_ca_cert'] =\ diff --git a/tacker/sol_refactored/common/exceptions.py b/tacker/sol_refactored/common/exceptions.py index f414b224e..5938ed5b0 100644 --- a/tacker/sol_refactored/common/exceptions.py +++ b/tacker/sol_refactored/common/exceptions.py @@ -356,3 +356,8 @@ class K8sResourceNotFound(SolHttpError404): class K8sInvalidManifestFound(SolHttpError400): message = _("Invalid manifest found.") + + +class OIDCAuthFailed(SolHttpError400): + message = _("OIDC authentication and authorization failed." + " Detail: %(detail)s") diff --git a/tacker/sol_refactored/common/oidc_utils.py b/tacker/sol_refactored/common/oidc_utils.py new file mode 100644 index 000000000..52e056934 --- /dev/null +++ b/tacker/sol_refactored/common/oidc_utils.py @@ -0,0 +1,57 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +"""Utilities functions for get openid token.""" + +import requests + +from tacker.sol_refactored.common.exceptions import OIDCAuthFailed + + +def get_id_token_with_password_grant( + token_endpoint, username, password, client_id, + client_secret=None, ssl_ca_cert=None, timeout=20): + + if not token_endpoint or not username or not password or not client_id: + raise OIDCAuthFailed(detail='token_endpoint, username, password,' + ' client_id can not be empty.') + + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + body = { + 'grant_type': 'password', + 'scope': 'openid', + 'client_id': client_id, + 'client_secret': client_secret, + 'username': username, + 'password': password + } + + verify = ssl_ca_cert if ssl_ca_cert else False + + try: + resp = requests.post(token_endpoint, headers=headers, data=body, + verify=verify, timeout=timeout) + + if (resp.status_code == 200 + and resp.headers['Content-Type'] == 'application/json'): + return resp.json()['id_token'] + + raise OIDCAuthFailed( + detail=f'response code: {resp.status_code}, body: {resp.text}') + except requests.exceptions.RequestException as exc: + raise OIDCAuthFailed(detail=str(exc)) diff --git a/tacker/sol_refactored/common/vim_utils.py b/tacker/sol_refactored/common/vim_utils.py index 9fa13d352..af0bdfc56 100644 --- a/tacker/sol_refactored/common/vim_utils.py +++ b/tacker/sol_refactored/common/vim_utils.py @@ -76,7 +76,15 @@ def vim_to_conn_info(vim): accessInfo=access_info ) if vim['vim_type'] == "kubernetes": # k8s - if vim_auth['username'] and vim_auth['password']: + if 'oidc_token_url' in vim_auth: + access_info = { + 'oidc_token_url': vim_auth.get('oidc_token_url'), + 'username': vim_auth.get('username'), + 'password': vim_auth.get('password'), + 'client_id': vim_auth.get('client_id'), + 'client_secret': vim_auth.get('client_secret') + } + elif vim_auth.get('username') and vim_auth.get('password'): access_info = { 'username': vim_auth['username'], 'password': vim_auth['password'] diff --git a/tacker/sol_refactored/infra_drivers/kubernetes/kubernetes_utils.py b/tacker/sol_refactored/infra_drivers/kubernetes/kubernetes_utils.py index 830e53aee..cba103f0b 100644 --- a/tacker/sol_refactored/infra_drivers/kubernetes/kubernetes_utils.py +++ b/tacker/sol_refactored/infra_drivers/kubernetes/kubernetes_utils.py @@ -24,6 +24,7 @@ from oslo_log import log as logging import yaml from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import oidc_utils from tacker.sol_refactored.infra_drivers.kubernetes import kubernetes_resource @@ -84,6 +85,7 @@ def is_match_pod_naming_rule(rsc_kind, rsc_name, pod_name): def get_k8s_reses_from_json_files(target_k8s_files, vnfd, k8s_api_client, namespace): + k8s_resources = [] for target_k8s_file in target_k8s_files: @@ -156,21 +158,36 @@ class AuthContextManager: k8s_config = client.Configuration() k8s_config.host = self.vim_info.interfaceInfo['endpoint'] - if ('username' in self.vim_info.accessInfo and - self.vim_info.accessInfo.get('password') is not None): - k8s_config.username = self.vim_info.accessInfo['username'] - k8s_config.password = self.vim_info.accessInfo['password'] - basic_token = k8s_config.get_basic_auth_token() - k8s_config.api_key['authorization'] = basic_token - - if 'bearer_token' in self.vim_info.accessInfo: - k8s_config.api_key_prefix['authorization'] = 'Bearer' - k8s_config.api_key['authorization'] = self.vim_info.accessInfo[ - 'bearer_token'] - if 'ssl_ca_cert' in self.vim_info.interfaceInfo: self._create_ca_cert_file( self.vim_info.interfaceInfo['ssl_ca_cert']) + + if 'oidc_token_url' in self.vim_info.accessInfo: + # Obtain a openid token from openid provider + id_token = oidc_utils.get_id_token_with_password_grant( + self.vim_info.accessInfo.get('oidc_token_url'), + self.vim_info.accessInfo.get('username'), + self.vim_info.accessInfo.get('password'), + self.vim_info.accessInfo.get('client_id'), + client_secret=self.vim_info.accessInfo.get('client_secret'), + ssl_ca_cert=self.ca_cert_file + ) + k8s_config.api_key_prefix['authorization'] = 'Bearer' + k8s_config.api_key['authorization'] = id_token + else: + if ('username' in self.vim_info.accessInfo and + self.vim_info.accessInfo.get('password') is not None): + k8s_config.username = self.vim_info.accessInfo['username'] + k8s_config.password = self.vim_info.accessInfo['password'] + basic_token = k8s_config.get_basic_auth_token() + k8s_config.api_key['authorization'] = basic_token + + if 'bearer_token' in self.vim_info.accessInfo: + k8s_config.api_key_prefix['authorization'] = 'Bearer' + k8s_config.api_key['authorization'] = self.vim_info.accessInfo[ + 'bearer_token'] + + if self.ca_cert_file: k8s_config.ssl_ca_cert = self.ca_cert_file k8s_config.verify_ssl = True else: diff --git a/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml b/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml new file mode 100644 index 000000000..1d11c96f6 --- /dev/null +++ b/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml @@ -0,0 +1,9 @@ +auth_url: "https://127.0.0.1:6443" +project_name: "default" +oidc_token_url: "https://127.0.0.1:8443/realms/oidc/protocol/openid-connect/token" +username: "end-user" +password: "end-user" +client_id: "tacker" +client_secret: "K0Zp5dvdOFhZ7W9PVNZn14omW9NmCQvQ" +ssl_ca_cert: None +type: "kubernetes" diff --git a/tacker/tests/functional/sol_kubernetes_oidc_auth/__init__.py b/tacker/tests/functional/sol_kubernetes_oidc_auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/functional/sol_kubernetes_oidc_auth/test_vim.py b/tacker/tests/functional/sol_kubernetes_oidc_auth/test_vim.py new file mode 100644 index 000000000..820025140 --- /dev/null +++ b/tacker/tests/functional/sol_kubernetes_oidc_auth/test_vim.py @@ -0,0 +1,147 @@ +# Copyright (C) 2022 FUJITSU +# All Rights Reserved. +# +# 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 yaml + +from tackerclient.common import exceptions + +from tacker.tests.functional import base +from tacker.tests.utils import read_file + +SECRET_PASSWORD = '***' + + +class VimTest(base.BaseTackerTest): + + @classmethod + def generate_vim_info_oidc_for_creation(cls): + + data = yaml.safe_load(read_file('local-k8s-vim-oidc.yaml')) + auth_cred = {'oidc_token_url': data['oidc_token_url'], + 'username': data['username'], + 'password': data['password'], + 'client_id': data['client_id'], + 'client_secret': data['client_secret']} + if 'ssl_ca_cert' in data: + auth_cred['ssl_ca_cert'] = data['ssl_ca_cert'] + return {'vim': {'name': 'VIM-OIDC-AUTH', + 'description': 'Kubernetes VIM with oidc auth', + 'type': 'kubernetes', + 'auth_url': data['auth_url'], + 'auth_cred': auth_cred, + 'vim_project': {'name': 'default'}, + 'is_default': False}} + + @classmethod + def generate_vim_info_oidc_for_update(cls): + + data = yaml.safe_load(read_file('local-k8s-vim-oidc.yaml')) + auth_cred = {'oidc_token_url': data['oidc_token_url'], + 'username': data['username'], + 'password': data['password'], + 'client_id': data['client_id'], + 'client_secret': data['client_secret']} + if 'ssl_ca_cert' in data: + auth_cred['ssl_ca_cert'] = data['ssl_ca_cert'] + return {'vim': {'name': 'VIM-OIDC-AUTH', + 'description': 'Kubernetes VIM with oidc auth', + 'auth_cred': auth_cred}} + + @classmethod + def generate_vim_info_token_for_update(cls): + + data = yaml.safe_load(read_file('local-k8s-vim.yaml')) + auth_cred = {'bearer_token': data['bearer_token']} + if 'ssl_ca_cert' in data: + auth_cred['ssl_ca_cert'] = data['ssl_ca_cert'] + return {'vim': {'name': 'VIM-BEARER-TOKEN', + 'description': 'Kubernetes VIM with bearer token', + 'auth_cred': auth_cred}} + + def assert_vim_auth_oidc(self, vim_auth_req, vim_auth_res): + unexpected_attrs = {'bearer_token'} + # check only specified attributes exist + self.assertNotIn(unexpected_attrs, vim_auth_res) + self.assertEqual(vim_auth_req['oidc_token_url'], + vim_auth_res['oidc_token_url']) + self.assertEqual(vim_auth_req['username'], + vim_auth_res['username']) + self.assertEqual(SECRET_PASSWORD, + vim_auth_res['password']) + self.assertEqual(vim_auth_req['client_id'], + vim_auth_res['client_id']) + self.assertEqual(SECRET_PASSWORD, + vim_auth_res['client_secret']) + + def assert_vim_auth_token(self, vim_auth_res): + unexpected_attrs = {'oidc_token_url', 'username', 'password', + 'client_id', 'client_secret'} + # check only specified attributes exist + self.assertNotIn(unexpected_attrs, vim_auth_res) + self.assertEqual(SECRET_PASSWORD, + vim_auth_res['bearer_token']) + + def test_vim_creation_update_with_oidc_auth(self): + + vim_oidc_create = self.generate_vim_info_oidc_for_creation() + vim_oidc_update = self.generate_vim_info_oidc_for_update() + vim_token_update = self.generate_vim_info_token_for_update() + + # Register vim + vim_res = self.client.create_vim(vim_oidc_create) + vim_id = vim_res['vim']['id'] + self.assert_vim_auth_oidc(vim_oidc_create['vim']['auth_cred'], + vim_res['vim']['auth_cred']) + # Read vim + vim_show_res = self.client.show_vim(vim_id) + self.assert_vim_auth_oidc(vim_oidc_create['vim']['auth_cred'], + vim_show_res['vim']['auth_cred']) + + # Update vim (oidc -> token) + vim_update = self.client.update_vim(vim_id, vim_token_update) + self.assert_vim_auth_token(vim_update['vim']['auth_cred']) + + # Read vim + vim_show_res = self.client.show_vim(vim_id) + self.assert_vim_auth_token(vim_show_res['vim']['auth_cred']) + + # Update vim (token -> oidc) + vim_update = self.client.update_vim(vim_id, vim_oidc_update) + self.assert_vim_auth_oidc(vim_oidc_update['vim']['auth_cred'], + vim_update['vim']['auth_cred']) + + # Read vim + vim_show_res = self.client.show_vim(vim_id) + self.assert_vim_auth_oidc(vim_oidc_update['vim']['auth_cred'], + vim_show_res['vim']['auth_cred']) + + # Delete vim + self.client.delete_vim(vim_id) + + def test_vim_creation_with_bad_oidc_auth_info(self): + + vim_oidc = self.generate_vim_info_oidc_for_creation() + vim_oidc['vim']['auth_cred']['password'] = 'bad password' + vim_oidc['vim']['auth_cred']['client_secret'] = 'bad secret' + + # Register vim + exc = self.assertRaises(exceptions.InternalServerError, + self.client.create_vim, + vim_oidc) + message = ('OIDC authentication and authorization failed. ' + 'Detail: response code: 401, body: ' + '{"error":"unauthorized_client",' + '"error_description":"Invalid client secret"}') + self.assertEqual(message, exc.message) diff --git a/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v1/__init__.py b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v1/test_kubernetes_oidc_auth.py b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v1/test_kubernetes_oidc_auth.py new file mode 100644 index 000000000..3afd11ec3 --- /dev/null +++ b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v1/test_kubernetes_oidc_auth.py @@ -0,0 +1,52 @@ +# Copyright (C) 2022 FUJITSU +# All Rights Reserved. +# +# 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. + +from tacker.tests.functional.sol_kubernetes.vnflcm import base + + +class VnfLcmKubernetesOidcTest(base.BaseVnfLcmKubernetesTest): + + @classmethod + def setUpClass(cls): + super(VnfLcmKubernetesOidcTest, cls).setUpClass() + vnf_package_id, cls.vnfd_id = \ + cls._create_and_upload_vnf_package( + cls, cls.tacker_client, "test_cnf_scale", + {"key": "sample_scale_functional"}) + cls.vnf_package_ids.append(vnf_package_id) + + @classmethod + def tearDownClass(cls): + super(VnfLcmKubernetesOidcTest, cls).tearDownClass() + + def test_basic_lcmsV1_with_oidc_auth(self): + """Test CNF LCM with OIDC auth + + This test will cover the instantaite, scale, terminate operation + with OIDC auth. + """ + vnf_instance_name = "cnf_lcmv1_with_oidc_auth" + vnf_instance_description = "cnf lcm with oidc auth" + inst_additional_param = { + "lcm-kubernetes-def-files": [ + "Files/kubernetes/deployment_scale.yaml"]} + vnf_instance = self._create_and_instantiate_vnf_instance( + self.vnfd_id, "scalingsteps", vnf_instance_name, + vnf_instance_description, inst_additional_param) + # Use flavour_id scalingsteps that is set to delta_num=1 + self._test_scale_out_and_in( + vnf_instance, "vdu1_aspect", number_of_steps=1) + self._terminate_vnf_instance(vnf_instance['id']) + self._delete_vnf_instance(vnf_instance['id']) diff --git a/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/__init__.py b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/base_v2.py b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/base_v2.py new file mode 100644 index 000000000..a974f1b60 --- /dev/null +++ b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/base_v2.py @@ -0,0 +1,89 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from oslo_config import cfg +import yaml + +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored import objects +from tacker.tests.functional.sol_kubernetes_v2 import base_v2 +from tacker.tests import utils as base_utils +from tacker import version + +VNF_PACKAGE_UPLOAD_TIMEOUT = 300 +VNF_INSTANTIATE_TIMEOUT = 600 +VNF_TERMINATE_TIMEOUT = 600 +RETRY_WAIT_TIME = 5 + + +class BaseVnfLcmKubernetesV2OidcTest(base_v2.BaseVnfLcmKubernetesV2Test): + + @classmethod + def setUpClass(cls): + super(base_v2.BaseVnfLcmKubernetesV2Test, cls).setUpClass() + """Base test case class for SOL v2 kubernetes Oidc functional tests.""" + + cfg.CONF(args=['--config-file', '/etc/tacker/tacker.conf'], + project='tacker', + version='%%prog %s' % version.version_info.release_string()) + objects.register_all() + + cls.k8s_vim_info = cls.get_k8s_vim_info() + + vim_info = cls.get_vim_info() + auth = http_client.KeystonePasswordAuthHandle( + auth_url=vim_info.interfaceInfo['endpoint'], + username=vim_info.accessInfo['username'], + password=vim_info.accessInfo['password'], + project_name=vim_info.accessInfo['project'], + user_domain_name=vim_info.accessInfo['userDomain'], + project_domain_name=vim_info.accessInfo['projectDomain'] + ) + cls.tacker_client = http_client.HttpClient(auth) + + @classmethod + def get_k8s_vim_info(cls): + vim_params = yaml.safe_load( + base_utils.read_file('local-k8s-vim-oidc.yaml')) + + vim_info = objects.VimConnectionInfo( + interfaceInfo={'endpoint': vim_params['auth_url']}, + accessInfo={ + 'oidc_token_url': vim_params['oidc_token_url'], + 'username': vim_params['username'], + 'password': vim_params['password'], + 'client_id': vim_params['client_id'], + } + ) + # if ssl_ca_cert is set, add it to vim_info.interfaceInfo + if vim_params.get('ssl_ca_cert'): + vim_info.interfaceInfo['ssl_ca_cert'] = vim_params['ssl_ca_cert'] + # if client_secret is set, add it to vim_info.accessInfo + if vim_params.get('client_secret'): + vim_info.accessInfo['client_secret'] = vim_params['client_secret'] + + return vim_info + + @classmethod + def get_k8s_vim_id(cls): + vim_list = cls.list_vims(cls) + if len(vim_list.values()) == 0: + assert False, "vim_list is Empty: Default VIM is missing" + + for vim_list in vim_list.values(): + for vim in vim_list: + if vim['name'] == 'vim-kubernetes-oidc': + return vim['id'] + return None diff --git a/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/test_kubernetes_oidc_auth.py b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/test_kubernetes_oidc_auth.py new file mode 100644 index 000000000..ce0fce308 --- /dev/null +++ b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/test_kubernetes_oidc_auth.py @@ -0,0 +1,255 @@ +# Copyright (C) 2022 FUJITSU +# All Rights Reserved. +# +# 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 ddt +import os +import time + +from tacker.tests.functional.sol_kubernetes_oidc_auth.vnflcm_v2 import base_v2 +from tacker.tests.functional.sol_kubernetes_v2 import paramgen + + +@ddt.ddt +class VnfLcmKubernetesTest(base_v2.BaseVnfLcmKubernetesV2OidcTest): + + @classmethod + def setUpClass(cls): + super(VnfLcmKubernetesTest, cls).setUpClass() + + cur_dir = os.path.dirname(__file__) + + test_instantiate_cnf_resources_path = os.path.join(cur_dir, + "../../sol_kubernetes_v2/samples/test_instantiate_cnf_resources") + cls.vnf_pkg_1, cls.vnfd_id_1 = cls.create_vnf_package( + test_instantiate_cnf_resources_path) + + @classmethod + def tearDownClass(cls): + super(VnfLcmKubernetesTest, cls).tearDownClass() + + cls.delete_vnf_package(cls.vnf_pkg_1) + + def setUp(self): + super(VnfLcmKubernetesTest, self).setUp() + + def test_basic_lcmsV2_with_oidc_auth(self): + """Test CNF LCM v2 with OIDC auth + + * About attributes: + Omit except for required attributes. + Only the following cardinality attributes are set. + - 1 + - 1..N (1) + + * About LCM operations: + This test includes the following operations. + - 1. Create a new VNF instance resource + - 2. Instantiate a VNF instance + - 3. Show VNF instance + - 4. Terminate a VNF instance + - 5. Delete a VNF instance + """ + + # 1. Create a new VNF instance resource + # NOTE: extensions and vnfConfigurableProperties are omitted + # because they are commented out in etsi_nfv_sol001. + expected_inst_attrs = [ + 'id', + 'vnfInstanceName', + 'vnfInstanceDescription', + 'vnfdId', + 'vnfProvider', + 'vnfProductName', + 'vnfSoftwareVersion', + 'vnfdVersion', + # 'vnfConfigurableProperties', # omitted + # 'vimConnectionInfo', # omitted + 'instantiationState', + # 'instantiatedVnfInfo', # omitted + 'metadata', + # 'extensions', # omitted + '_links' + ] + create_req = paramgen.test_instantiate_cnf_resources_create( + self.vnfd_id_1) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(201, resp.status_code) + self.check_resp_headers_in_create(resp) + self.check_resp_body(body, expected_inst_attrs) + inst_id = body['id'] + + # check usageState of VNF Package + usage_state = self.get_vnf_package(self.vnf_pkg_1)['usageState'] + self.assertEqual('IN_USE', usage_state) + + # 2. Instantiate a VNF instance + vim_id = self.get_k8s_vim_id() + instantiate_req = paramgen.min_sample_instantiate(vim_id) + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(202, resp.status_code) + self.check_resp_headers_in_operation_task(resp) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # 3. Show VNF instance + additional_inst_attrs = [ + 'vimConnectionInfo', + 'instantiatedVnfInfo' + ] + expected_inst_attrs.extend(additional_inst_attrs) + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, expected_inst_attrs) + + # check vnfc_resource_info + vnfc_resource_infos = body['instantiatedVnfInfo'].get( + 'vnfcResourceInfo') + self.assertEqual(1, len(vnfc_resource_infos)) + + # 4. Terminate a VNF instance + terminate_req = paramgen.min_sample_terminate() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(202, resp.status_code) + self.check_resp_headers_in_operation_task(resp) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # wait a bit because there is a bit time lag between lcmocc DB + # update and terminate completion. + time.sleep(10) + + # 5. Delete a VNF instance + resp, body = self.delete_vnf_instance(inst_id) + self.assertEqual(204, resp.status_code) + self.check_resp_headers_in_delete(resp) + + # check deletion of VNF instance + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(404, resp.status_code) + + # check usageState of VNF Package + usage_state = self.get_vnf_package(self.vnf_pkg_1).get('usageState') + self.assertEqual('NOT_IN_USE', usage_state) + + def test_instantiationV2_with_bad_oidc_auth_info(self): + """Test CNF LCM v2 with bad OIDC auth + + * About attributes: + Omit except for required attributes. + Only the following cardinality attributes are set. + - 1 + - 1..N (1) + + * About LCM operations: + This test includes the following operations. + - 1. Create a new VNF instance resource + - 2. Instantiate a VNF instance(FAIL) + """ + + # 1. Create a new VNF instance resource + # NOTE: extensions and vnfConfigurableProperties are omitted + # because they are commented out in etsi_nfv_sol001. + expected_inst_attrs = [ + 'id', + 'vnfInstanceName', + 'vnfInstanceDescription', + 'vnfdId', + 'vnfProvider', + 'vnfProductName', + 'vnfSoftwareVersion', + 'vnfdVersion', + # 'vnfConfigurableProperties', # omitted + # 'vimConnectionInfo', # omitted + 'instantiationState', + # 'instantiatedVnfInfo', # omitted + 'metadata', + # 'extensions', # omitted + '_links' + ] + create_req = paramgen.test_instantiate_cnf_resources_create( + self.vnfd_id_1) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(201, resp.status_code) + self.check_resp_headers_in_create(resp) + self.check_resp_body(body, expected_inst_attrs) + inst_id = body['id'] + + # check usageState of VNF Package + usage_state = self.get_vnf_package(self.vnf_pkg_1)['usageState'] + self.assertEqual('IN_USE', usage_state) + + # 2. Instantiate a VNF instance + k8s_vim_info = copy.deepcopy(self.k8s_vim_info) + k8s_vim_info.accessInfo['client_id'] = 'badclient' + k8s_vim_info.accessInfo['password'] = 'badpassword' + instantiate_req = paramgen.min_sample_instantiate_with_vim_info( + k8s_vim_info) + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(202, resp.status_code) + self.check_resp_headers_in_operation_task(resp) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_failed_temp(lcmocc_id) + + # 3. Show VNF instance + additional_inst_attrs = [ + 'vimConnectionInfo', + 'instantiatedVnfInfo' + ] + expected_inst_attrs.extend(additional_inst_attrs) + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.assertEqual('NOT_INSTANTIATED', body['instantiationState']) + + # 4. Fail instantiate operation + expected_inst_attrs_fail = [ + 'id', + 'operationState', + 'stateEnteredTime', + 'startTime', + 'vnfInstanceId', + 'grantId', + 'operation', + 'isAutomaticInvocation', + 'operationParams', + 'isCancelPending', + 'error', + '_links' + ] + resp, body = self.fail_lcmocc(lcmocc_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, expected_inst_attrs_fail) + resp, body = self.show_lcmocc(lcmocc_id) + self.assertEqual(200, resp.status_code) + self.assertEqual('FAILED', body['operationState']) + + # 5. Delete a VNF instance + resp, body = self.delete_vnf_instance(inst_id) + self.assertEqual(204, resp.status_code) + self.check_resp_headers_in_delete(resp) + + # check deletion of VNF instance + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(404, resp.status_code) + + # check usageState of VNF Package + usage_state = self.get_vnf_package(self.vnf_pkg_1).get('usageState') + self.assertEqual('NOT_IN_USE', usage_state) diff --git a/tacker/tests/functional/sol_kubernetes_v2/paramgen.py b/tacker/tests/functional/sol_kubernetes_v2/paramgen.py index e8eaad27d..f5deb9327 100644 --- a/tacker/tests/functional/sol_kubernetes_v2/paramgen.py +++ b/tacker/tests/functional/sol_kubernetes_v2/paramgen.py @@ -178,6 +178,27 @@ def min_sample_instantiate(vim_id_1): } +def min_sample_instantiate_with_vim_info(k8s_vim_info): + + vim_1 = { + "vimId": uuidutils.generate_uuid(), + "vimType": "kubernetes", + "accessInfo": k8s_vim_info.accessInfo, + "interfaceInfo": k8s_vim_info.interfaceInfo + } + return { + "flavourId": "simple", + "vimConnectionInfo": { + "vim1": vim_1, + }, + "additionalParams": { + "lcm-kubernetes-def-files": [ + "Files/kubernetes/pod.yaml" + ] + } + } + + def min_sample_terminate(): # Omit except for required attributes # NOTE: Only the following cardinality attributes are set. diff --git a/tacker/tests/unit/common/container/__init__.py b/tacker/tests/unit/common/container/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/unit/common/container/test_kubernetes_utils.py b/tacker/tests/unit/common/container/test_kubernetes_utils.py index 60c85b51b..4ed8cdffa 100644 --- a/tacker/tests/unit/common/container/test_kubernetes_utils.py +++ b/tacker/tests/unit/common/container/test_kubernetes_utils.py @@ -12,8 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. -import cryptography.fernet as fernet +from cryptography import fernet from kubernetes import client +from unittest import mock +from unittest import skip from tacker.common.container import kubernetes_utils from tacker.tests import base @@ -25,6 +27,10 @@ class TestKubernetesHTTPAPI(base.BaseTestCase): super(TestKubernetesHTTPAPI, self).setUp() self.kubernetes_http_api = kubernetes_utils.KubernetesHTTPAPI() + # NOTE(Yao Qibin): This unit test will be executed after __init__.py + # is added. And this unit test fails because the beta1 api is old + # and deprecated. To avoid errors, comment out the following test code. + @skip('Delete deprecated api test') def test_get_extension_api_client(self): auth = {"auth_url": "auth", 'bearer_token': 'token'} extensions_v1_beta1_api = \ @@ -44,3 +50,84 @@ class TestKubernetesHTTPAPI(base.BaseTestCase): fernet_key, fernet_obj = self.kubernetes_http_api.create_fernet_key() self.assertEqual(len(fernet_key), 44) self.assertIsInstance(fernet_obj, fernet.Fernet) + + @mock.patch('tacker.common.oidc_utils.get_id_token_with_password_grant') + def test_get_k8s_client_oidc_auth(self, mock_get_token): + mock_get_token.return_value = 'id_token' + + auth_plugin = { + 'auth_url': 'auth_url', + 'oidc_token_url': 'oidc_token_url', + 'client_id': 'client_id', + 'client_secret': 'client_secret', + 'username': 'username', + 'password': 'password', + 'ca_cert_file': 'ca_cert_file' + } + k8s_client = self.kubernetes_http_api.get_k8s_client(auth_plugin) + k8s_client_config = k8s_client.configuration + self.assertEqual('auth_url', k8s_client_config.host) + self.assertDictEqual({'authorization': 'Bearer'}, + k8s_client_config.api_key_prefix) + self.assertDictEqual({'authorization': 'id_token'}, + k8s_client_config.api_key) + self.assertEqual('ca_cert_file', k8s_client_config.ssl_ca_cert) + self.assertTrue(k8s_client_config.verify_ssl) + + @mock.patch('tacker.common.oidc_utils.get_id_token_with_password_grant') + def test_get_k8s_client_oidc_auth_no_cert(self, mock_get_token): + mock_get_token.return_value = 'id_token' + + auth_plugin = { + 'auth_url': 'auth_url', + 'oidc_token_url': 'oidc_token_url', + 'client_id': 'client_id', + 'client_secret': 'client_secret', + 'username': 'username', + 'password': 'password' + } + k8s_client = self.kubernetes_http_api.get_k8s_client(auth_plugin) + k8s_client_config = k8s_client.configuration + self.assertEqual('auth_url', k8s_client_config.host) + self.assertDictEqual({'authorization': 'Bearer'}, + k8s_client_config.api_key_prefix) + self.assertDictEqual({'authorization': 'id_token'}, + k8s_client_config.api_key) + self.assertFalse(k8s_client_config.verify_ssl) + + def test_get_k8s_client_oidc_auth_id_token_exsits(self): + + auth_plugin = { + 'auth_url': 'auth_url', + 'oidc_token_url': 'oidc_token_url', + 'client_id': 'client_id', + 'client_secret': 'client_secret', + 'username': 'username', + 'password': 'password', + 'ca_cert_file': 'ca_cert_file', + 'id_token': 'id_token' + } + k8s_client = self.kubernetes_http_api.get_k8s_client(auth_plugin) + k8s_client_config = k8s_client.configuration + self.assertEqual('auth_url', k8s_client_config.host) + self.assertDictEqual({'authorization': 'Bearer'}, + k8s_client_config.api_key_prefix) + self.assertDictEqual({'authorization': 'id_token'}, + k8s_client_config.api_key) + self.assertEqual('ca_cert_file', k8s_client_config.ssl_ca_cert) + self.assertTrue(k8s_client_config.verify_ssl) + + def test_get_k8s_client_service_account_token_auth(self): + + auth_plugin = { + 'auth_url': 'auth_url', + 'bearer_token': 'bearer_token' + } + k8s_client = self.kubernetes_http_api.get_k8s_client(auth_plugin) + k8s_client_config = k8s_client.configuration + self.assertEqual('auth_url', k8s_client_config.host) + self.assertDictEqual({'authorization': 'Bearer'}, + k8s_client_config.api_key_prefix) + self.assertDictEqual({'authorization': 'bearer_token'}, + k8s_client_config.api_key) + self.assertFalse(k8s_client_config.verify_ssl) diff --git a/tacker/tests/unit/common/test_oidc_utils.py b/tacker/tests/unit/common/test_oidc_utils.py new file mode 100644 index 000000000..a413099c2 --- /dev/null +++ b/tacker/tests/unit/common/test_oidc_utils.py @@ -0,0 +1,123 @@ +# Copyright (c) 2012 OpenStack Foundation. +# +# 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 json +import requests +from unittest import mock + +from tacker.common import oidc_utils +from tacker.extensions.vnfm import OIDCAuthFailed +from tacker.tests import base + + +class FakeResponse: + + def __init__(self, status_code, body, headers=None): + self.status_code = status_code + self.headers = headers + self.text = body + + def json(self): + return json.loads(self.text) + + +class TestOidcUtils(base.BaseTestCase): + + @mock.patch('requests.post') + def test_get_id_token_with_password_grant(self, mock_post): + mock_post.return_value = FakeResponse( + 200, + '{"id_token": "id token"}', + headers={'Content-Type': 'application/json'} + ) + id_token = oidc_utils.get_id_token_with_password_grant( + 'oidc_token_url', + 'username', + 'password', + 'client_id', + client_secret='client_secret', + ssl_ca_cert='ssl_ca_cert' + ) + self.assertEqual(id_token, 'id token') + + @mock.patch('requests.post') + def test_get_id_token_with_password_grant_no_option_param(self, mock_post): + mock_post.return_value = FakeResponse( + 200, + '{"id_token": "id token"}', + headers={'Content-Type': 'application/json'} + ) + id_token = oidc_utils.get_id_token_with_password_grant( + 'oidc_token_url', + 'username', + 'password', + 'client_id' + ) + self.assertEqual(id_token, 'id token') + + def test_get_id_token_with_password_grant_required_param_is_none(self): + exc = self.assertRaises( + OIDCAuthFailed, + oidc_utils.get_id_token_with_password_grant, + 'oidc_token_url', + 'username', + 'password', + None) + + detail = ('token_endpoint, username, password,' + ' client_id can not be empty.') + msg = f'OIDC authentication and authorization failed. Detail: {detail}' + self.assertEqual(msg, exc.format_message()) + + @mock.patch('requests.post') + def test_get_id_token_with_password_grant_401(self, mock_post): + mock_post.return_value = FakeResponse( + 401, + '{"error": "invalid_grant", ' + 'error_description": "Invalid user credentials"}' + ) + exc = self.assertRaises( + OIDCAuthFailed, + oidc_utils.get_id_token_with_password_grant, + 'oidc_token_url', + 'username', + 'password', + 'client_id', + client_secret='client_secret', + ssl_ca_cert='ssl_ca_cert') + + detail = ('response code: 401, body: {"error": "invalid_grant", ' + 'error_description": "Invalid user credentials"}') + msg = f'OIDC authentication and authorization failed. Detail: {detail}' + self.assertEqual(msg, exc.format_message()) + + @mock.patch('requests.post') + def test_get_id_token_with_password_grant_request_exception( + self, mock_post): + mock_post.side_effect = requests.exceptions.RequestException( + 'Connection refused' + ) + exc = self.assertRaises( + OIDCAuthFailed, + oidc_utils.get_id_token_with_password_grant, + 'oidc_token_url', + 'username', + 'password', + 'client_id', + client_secret='client_secret', + ssl_ca_cert='ssl_ca_cert') + + detail = 'Connection refused' + msg = f'OIDC authentication and authorization failed. Detail: {detail}' + self.assertEqual(msg, exc.format_message()) diff --git a/tacker/tests/unit/nfvo/test_nfvo_plugin.py b/tacker/tests/unit/nfvo/test_nfvo_plugin.py index 04b498dae..78665725c 100644 --- a/tacker/tests/unit/nfvo/test_nfvo_plugin.py +++ b/tacker/tests/unit/nfvo/test_nfvo_plugin.py @@ -280,6 +280,79 @@ class TestNfvoPlugin(db_base.SqlTestCase): session.add(vim_auth_db) session.flush() + def _insert_dummy_vim_k8s_user(self): + session = self.context.session + vim_db = nfvo_db.Vim( + id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + name='fake_vim', + description='fake_vim_description', + type='kubernetes', + status='Active', + deleted_at=datetime.min, + placement_attr={'regions': ['RegionOne']}) + vim_auth_db = nfvo_db.VimAuth( + vim_id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + password='encrypted_pw', + auth_url='http://localhost:6443', + vim_project={'name': 'test_project'}, + auth_cred={'username': 'test_user', + 'key_type': 'barbican_key', + 'secret_uuid': 'fake-secret-uuid'}) + session.add(vim_db) + session.add(vim_auth_db) + session.flush() + + def _insert_dummy_vim_k8s_token(self): + session = self.context.session + vim_db = nfvo_db.Vim( + id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + name='fake_vim', + description='fake_vim_description', + type='kubernetes', + status='Active', + deleted_at=datetime.min, + placement_attr={'regions': ['RegionOne']}) + vim_auth_db = nfvo_db.VimAuth( + vim_id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + password='encrypted_pw', + auth_url='http://localhost:6443', + vim_project={'name': 'test_project'}, + auth_cred={'bearer_token': 'encrypted_token', + 'key_type': 'barbican_key', + 'secret_uuid': 'fake-secret-uuid'}) + session.add(vim_db) + session.add(vim_auth_db) + session.flush() + + def _insert_dummy_vim_k8s_oidc(self): + session = self.context.session + vim_db = nfvo_db.Vim( + id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + name='fake_vim', + description='fake_vim_description', + type='kubernetes', + status='Active', + deleted_at=datetime.min, + placement_attr={'regions': ['RegionOne']}) + vim_auth_db = nfvo_db.VimAuth( + vim_id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + password='encrypted_pw', + auth_url='http://localhost:6443', + vim_project={'name': 'test_project'}, + auth_cred={'username': 'oidc_user', + 'oidc_token_url': 'https://localhost:8443', + 'client_id': 'oidc_client', + 'client_secret': 'encrypted_secret', + 'ssl_ca_cert': 'cert_content', + 'key_type': 'barbican_key', + 'secret_uuid': 'fake-secret-uuid'}) + session.add(vim_db) + session.add(vim_auth_db) + session.flush() + def test_create_vim(self): vim_dict = utils.get_vim_obj() vim_type = 'openstack' @@ -308,6 +381,90 @@ class TestNfvoPlugin(db_base.SqlTestCase): self.assertEqual(False, res['is_default']) self.assertEqual('openstack', res['type']) + def test_create_vim_k8s_token(self): + vim_dict = {'vim': {'type': 'kubernetes', + 'auth_url': 'http://localhost/identity', + 'vim_project': {'name': 'test_project'}, + 'auth_cred': {'bearer_token': 'test_token'}, + 'name': 'VIM0', + 'tenant_id': 'test-project'}} + vim_type = 'kubernetes' + self._mock_driver_manager() + mock.patch('tacker.nfvo.nfvo_plugin.NfvoPlugin._get_vim_from_vnf', + side_effect=dummy_get_vim).start() + self.nfvo_plugin = nfvo_plugin.NfvoPlugin() + mock.patch('tacker.db.common_services.common_services_db_plugin.' + 'CommonServicesPluginDb.create_event' + ).start() + self._cos_db_plugin =\ + common_services_db_plugin.CommonServicesPluginDb() + res = self.nfvo_plugin.create_vim(self.context, vim_dict) + self._cos_db_plugin.create_event.assert_any_call( + self.context, evt_type=constants.RES_EVT_CREATE, res_id=mock.ANY, + res_state=mock.ANY, res_type=constants.RES_TYPE_VIM, + tstamp=mock.ANY) + self._driver_manager.invoke.assert_any_call( + vim_type, 'register_vim', vim_obj=vim_dict['vim']) + self.assertIsNotNone(res) + self.assertEqual(SECRET_PASSWORD, res['auth_cred']['bearer_token']) + self.assertIn('id', res) + self.assertIn('placement_attr', res) + self.assertIn('created_at', res) + self.assertIn('updated_at', res) + self.assertEqual(False, res['is_default']) + self.assertEqual(vim_type, res['type']) + + def test_create_vim_k8s_oidc(self): + vim_dict = {'vim': {'type': 'kubernetes', + 'auth_url': 'http://localhost/identity', + 'vim_project': {'name': 'test_project'}, + 'auth_cred': { + 'username': 'oidc_user', + 'password': 'oidc_password', + 'oidc_token_url': 'https://localhost:8443', + 'client_id': 'oidc_client', + 'client_secret': 'oidc_secret', + 'ssl_ca_cert': 'cert_content'}, + 'name': 'VIM0', + 'tenant_id': 'test-project'}} + vim_type = 'kubernetes' + vim_auth_username = vim_dict['vim']['auth_cred']['username'] + vim_auth_client_id = vim_dict['vim']['auth_cred']['client_id'] + vim_auth_oidc_url = vim_dict['vim']['auth_cred']['oidc_token_url'] + vim_auth_cert = vim_dict['vim']['auth_cred']['ssl_ca_cert'] + vim_project = vim_dict['vim']['vim_project'] + self._mock_driver_manager() + mock.patch('tacker.nfvo.nfvo_plugin.NfvoPlugin._get_vim_from_vnf', + side_effect=dummy_get_vim).start() + self.nfvo_plugin = nfvo_plugin.NfvoPlugin() + mock.patch('tacker.db.common_services.common_services_db_plugin.' + 'CommonServicesPluginDb.create_event' + ).start() + self._cos_db_plugin =\ + common_services_db_plugin.CommonServicesPluginDb() + res = self.nfvo_plugin.create_vim(self.context, vim_dict) + self._cos_db_plugin.create_event.assert_any_call( + self.context, evt_type=constants.RES_EVT_CREATE, res_id=mock.ANY, + res_state=mock.ANY, res_type=constants.RES_TYPE_VIM, + tstamp=mock.ANY) + self._driver_manager.invoke.assert_any_call( + vim_type, 'register_vim', vim_obj=vim_dict['vim']) + self.assertIsNotNone(res) + self.assertEqual(SECRET_PASSWORD, res['auth_cred']['password']) + self.assertEqual(vim_project, res['vim_project']) + self.assertEqual(vim_auth_username, res['auth_cred']['username']) + self.assertEqual(SECRET_PASSWORD, res['auth_cred']['password']) + self.assertEqual(vim_auth_oidc_url, res['auth_cred']['oidc_token_url']) + self.assertEqual(vim_auth_client_id, res['auth_cred']['client_id']) + self.assertEqual(SECRET_PASSWORD, res['auth_cred']['client_secret']) + self.assertEqual(vim_auth_cert, res['auth_cred']['ssl_ca_cert']) + self.assertIn('id', res) + self.assertIn('placement_attr', res) + self.assertIn('created_at', res) + self.assertIn('updated_at', res) + self.assertEqual(False, res['is_default']) + self.assertEqual(vim_type, res['type']) + def test_delete_vim(self): self._insert_dummy_vim() vim_type = 'openstack' @@ -418,6 +575,163 @@ class TestNfvoPlugin(db_base.SqlTestCase): res_state=mock.ANY, res_type=constants.RES_TYPE_VIM, tstamp=mock.ANY) + def test_update_vim_userpass_to_oidc(self): + vim_dict = {'vim': {'id': '6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'vim_project': {'name': 'new_project'}, + 'auth_cred': { + 'username': 'oidc_user', + 'password': 'oidc_password', + 'oidc_token_url': 'https://localhost:8443', + 'client_id': 'oidc_client', + 'client_secret': 'oidc_secret', + 'ssl_ca_cert': 'cert_content' + }}} + vim_type = 'kubernetes' + vim_auth_username = vim_dict['vim']['auth_cred']['username'] + vim_auth_client_id = vim_dict['vim']['auth_cred']['client_id'] + vim_auth_oidc_url = vim_dict['vim']['auth_cred']['oidc_token_url'] + vim_auth_cert = vim_dict['vim']['auth_cred']['ssl_ca_cert'] + vim_project = vim_dict['vim']['vim_project'] + self._insert_dummy_vim_k8s_user() + self.context.tenant_id = 'ad7ebc56538745a08ef7c5e97f8bd437' + old_vim_obj = self.nfvo_plugin._get_vim( + self.context, vim_dict['vim']['id']) + self._mock_driver_manager() + mock.patch('tacker.nfvo.nfvo_plugin.NfvoPlugin._get_vim_from_vnf', + side_effect=dummy_get_vim).start() + self.nfvo_plugin = nfvo_plugin.NfvoPlugin() + mock.patch('tacker.db.common_services.common_services_db_plugin.' + 'CommonServicesPluginDb.create_event' + ).start() + self._cos_db_plugin =\ + common_services_db_plugin.CommonServicesPluginDb() + res = self.nfvo_plugin.update_vim(self.context, vim_dict['vim']['id'], + vim_dict) + vim_obj = self.nfvo_plugin._get_vim( + self.context, vim_dict['vim']['id']) + vim_obj['updated_at'] = None + self._driver_manager.invoke.assert_called_with( + vim_type, 'delete_vim_auth', + vim_id=vim_obj['id'], + auth=old_vim_obj['auth_cred']) + self.assertIsNotNone(res) + self.assertIn('id', res) + self.assertIn('placement_attr', res) + self.assertEqual(vim_project, res['vim_project']) + self.assertEqual(vim_auth_username, res['auth_cred']['username']) + self.assertEqual(SECRET_PASSWORD, res['auth_cred']['password']) + self.assertEqual(vim_auth_oidc_url, res['auth_cred']['oidc_token_url']) + self.assertEqual(vim_auth_client_id, res['auth_cred']['client_id']) + self.assertEqual(SECRET_PASSWORD, res['auth_cred']['client_secret']) + self.assertEqual(vim_auth_cert, res['auth_cred']['ssl_ca_cert']) + self.assertIn('updated_at', res) + self._cos_db_plugin.create_event.assert_called_with( + self.context, evt_type=constants.RES_EVT_UPDATE, res_id=mock.ANY, + res_state=mock.ANY, res_type=constants.RES_TYPE_VIM, + tstamp=mock.ANY) + + def test_update_vim_token_to_oidc(self): + vim_dict = {'vim': {'id': '6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'vim_project': {'name': 'new_project'}, + 'auth_cred': { + 'username': 'oidc_user', + 'password': 'oidc_password', + 'oidc_token_url': 'https://localhost:8443', + 'client_id': 'oidc_client', + 'client_secret': 'oidc_secret', + 'ssl_ca_cert': 'cert_content' + }}} + vim_type = 'kubernetes' + vim_auth_username = vim_dict['vim']['auth_cred']['username'] + vim_auth_client_id = vim_dict['vim']['auth_cred']['client_id'] + vim_auth_oidc_url = vim_dict['vim']['auth_cred']['oidc_token_url'] + vim_auth_cert = vim_dict['vim']['auth_cred']['ssl_ca_cert'] + vim_project = vim_dict['vim']['vim_project'] + self._insert_dummy_vim_k8s_token() + self.context.tenant_id = 'ad7ebc56538745a08ef7c5e97f8bd437' + old_vim_obj = self.nfvo_plugin._get_vim( + self.context, vim_dict['vim']['id']) + self._mock_driver_manager() + mock.patch('tacker.nfvo.nfvo_plugin.NfvoPlugin._get_vim_from_vnf', + side_effect=dummy_get_vim).start() + self.nfvo_plugin = nfvo_plugin.NfvoPlugin() + mock.patch('tacker.db.common_services.common_services_db_plugin.' + 'CommonServicesPluginDb.create_event' + ).start() + self._cos_db_plugin =\ + common_services_db_plugin.CommonServicesPluginDb() + res = self.nfvo_plugin.update_vim(self.context, vim_dict['vim']['id'], + vim_dict) + vim_obj = self.nfvo_plugin._get_vim( + self.context, vim_dict['vim']['id']) + vim_obj['updated_at'] = None + self._driver_manager.invoke.assert_called_with( + vim_type, 'delete_vim_auth', + vim_id=vim_obj['id'], + auth=old_vim_obj['auth_cred']) + self.assertIsNotNone(res) + self.assertIn('id', res) + self.assertIn('placement_attr', res) + self.assertEqual(vim_project, res['vim_project']) + self.assertNotIn('bearer_token', res['auth_cred']) + self.assertEqual(vim_auth_username, res['auth_cred']['username']) + self.assertEqual(SECRET_PASSWORD, res['auth_cred']['password']) + self.assertEqual(vim_auth_oidc_url, res['auth_cred']['oidc_token_url']) + self.assertEqual(vim_auth_client_id, res['auth_cred']['client_id']) + self.assertEqual(SECRET_PASSWORD, res['auth_cred']['client_secret']) + self.assertEqual(vim_auth_cert, res['auth_cred']['ssl_ca_cert']) + self.assertIn('updated_at', res) + self._cos_db_plugin.create_event.assert_called_with( + self.context, evt_type=constants.RES_EVT_UPDATE, res_id=mock.ANY, + res_state=mock.ANY, res_type=constants.RES_TYPE_VIM, + tstamp=mock.ANY) + + def test_update_vim_oidc_to_token(self): + vim_dict = {'vim': {'id': '6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'vim_project': {'name': 'new_project'}, + 'auth_cred': { + 'bearer_token': 'bearer_token' + }}} + vim_type = 'kubernetes' + vim_project = vim_dict['vim']['vim_project'] + self._insert_dummy_vim_k8s_oidc() + self.context.tenant_id = 'ad7ebc56538745a08ef7c5e97f8bd437' + old_vim_obj = self.nfvo_plugin._get_vim( + self.context, vim_dict['vim']['id']) + self._mock_driver_manager() + mock.patch('tacker.nfvo.nfvo_plugin.NfvoPlugin._get_vim_from_vnf', + side_effect=dummy_get_vim).start() + self.nfvo_plugin = nfvo_plugin.NfvoPlugin() + mock.patch('tacker.db.common_services.common_services_db_plugin.' + 'CommonServicesPluginDb.create_event' + ).start() + self._cos_db_plugin =\ + common_services_db_plugin.CommonServicesPluginDb() + res = self.nfvo_plugin.update_vim(self.context, vim_dict['vim']['id'], + vim_dict) + vim_obj = self.nfvo_plugin._get_vim( + self.context, vim_dict['vim']['id']) + vim_obj['updated_at'] = None + self._driver_manager.invoke.assert_called_with( + vim_type, 'delete_vim_auth', + vim_id=vim_obj['id'], + auth=old_vim_obj['auth_cred']) + self.assertIsNotNone(res) + self.assertIn('id', res) + self.assertIn('placement_attr', res) + self.assertEqual(vim_project, res['vim_project']) + self.assertEqual(SECRET_PASSWORD, res['auth_cred']['bearer_token']) + self.assertNotIn('oidc_token_url', res['auth_cred']) + self.assertNotIn('client_id', res['auth_cred']) + self.assertNotIn('client_secret', res['auth_cred']) + self.assertNotIn('username', res['auth_cred']) + self.assertNotIn('password', res['auth_cred']) + self.assertIn('updated_at', res) + self._cos_db_plugin.create_event.assert_called_with( + self.context, evt_type=constants.RES_EVT_UPDATE, res_id=mock.ANY, + res_state=mock.ANY, res_type=constants.RES_TYPE_VIM, + tstamp=mock.ANY) + def _insert_dummy_vnffg_template(self): session = self.context.session vnffg_template = vnffg_db.VnffgTemplate( diff --git a/tacker/tests/unit/sol_refactored/common/test_oidc_utils.py b/tacker/tests/unit/sol_refactored/common/test_oidc_utils.py new file mode 100644 index 000000000..9373c97da --- /dev/null +++ b/tacker/tests/unit/sol_refactored/common/test_oidc_utils.py @@ -0,0 +1,123 @@ +# Copyright (c) 2012 OpenStack Foundation. +# +# 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 json +import requests +from unittest import mock + +from tacker.sol_refactored.common.exceptions import OIDCAuthFailed +from tacker.sol_refactored.common import oidc_utils +from tacker.tests import base + + +class FakeResponse: + + def __init__(self, status_code, body, headers=None): + self.status_code = status_code + self.headers = headers + self.text = body + + def json(self): + return json.loads(self.text) + + +class TestOidcUtils(base.BaseTestCase): + + @mock.patch('requests.post') + def test_get_id_token_with_password_grant(self, mock_post): + mock_post.return_value = FakeResponse( + 200, + '{"id_token": "id token"}', + headers={'Content-Type': 'application/json'} + ) + id_token = oidc_utils.get_id_token_with_password_grant( + 'oidc_token_url', + 'username', + 'password', + 'client_id', + client_secret='client_secret', + ssl_ca_cert='ssl_ca_cert' + ) + self.assertEqual(id_token, 'id token') + + @mock.patch('requests.post') + def test_get_id_token_with_password_grant_no_option_param(self, mock_post): + mock_post.return_value = FakeResponse( + 200, + '{"id_token": "id token"}', + headers={'Content-Type': 'application/json'} + ) + id_token = oidc_utils.get_id_token_with_password_grant( + 'oidc_token_url', + 'username', + 'password', + 'client_id' + ) + self.assertEqual(id_token, 'id token') + + def test_get_id_token_with_password_grant_required_param_is_none(self): + exc = self.assertRaises( + OIDCAuthFailed, + oidc_utils.get_id_token_with_password_grant, + 'oidc_token_url', + 'username', + 'password', + None) + + detail = ('token_endpoint, username, password,' + ' client_id can not be empty.') + msg = f'OIDC authentication and authorization failed. Detail: {detail}' + self.assertEqual(msg, exc.message % {'detail': detail}) + + @mock.patch('requests.post') + def test_get_id_token_with_password_grant_401(self, mock_post): + mock_post.return_value = FakeResponse( + 401, + '{"error": "invalid_grant", ' + 'error_description": "Invalid user credentials"}' + ) + exc = self.assertRaises( + OIDCAuthFailed, + oidc_utils.get_id_token_with_password_grant, + 'oidc_token_url', + 'username', + 'password', + 'client_id', + client_secret='client_secret', + ssl_ca_cert='ssl_ca_cert') + + detail = ('response code: 401, body: {"error": "invalid_grant", ' + 'error_description": "Invalid user credentials"}') + msg = f'OIDC authentication and authorization failed. Detail: {detail}' + self.assertEqual(msg, exc.message % {'detail': detail}) + + @mock.patch('requests.post') + def test_get_id_token_with_password_grant_request_exception( + self, mock_post): + mock_post.side_effect = requests.exceptions.RequestException( + 'Connection refused' + ) + exc = self.assertRaises( + OIDCAuthFailed, + oidc_utils.get_id_token_with_password_grant, + 'oidc_token_url', + 'username', + 'password', + 'client_id', + client_secret='client_secret', + ssl_ca_cert='ssl_ca_cert') + + detail = 'Connection refused' + msg = f'OIDC authentication and authorization failed. Detail: {detail}' + self.assertEqual(msg, exc.message % {'detail': detail}) diff --git a/tacker/tests/unit/sol_refactored/common/test_vim_utils.py b/tacker/tests/unit/sol_refactored/common/test_vim_utils.py index 68df17712..6329f7ea1 100644 --- a/tacker/tests/unit/sol_refactored/common/test_vim_utils.py +++ b/tacker/tests/unit/sol_refactored/common/test_vim_utils.py @@ -60,6 +60,18 @@ _vim_kubernetes_user = { "vim_type": "kubernetes", "vim_id": "kubernetes-2" } +_vim_kubernetes_oidc = { + "vim_auth": { + "username": "admin", + "password": "admin", + "auth_url": "https://127.0.0.1:6443", + "oidc_token_url": "https://127.0.0.1:8443", + "client_id": "tacker", + "client_secret": "K0Zp5dvdOFhZ7W9PVNZn14omW9NmCQvQ", + }, + "vim_type": "kubernetes", + "vim_id": "kubernetes-3" +} class TestVimUtils(base.BaseTestCase): @@ -99,6 +111,7 @@ class TestVimUtils(base.BaseTestCase): vim_openstack = _vim_openstack vim_kubernetes_1 = _vim_kubernetes_bearer_token vim_kubernetes_2 = _vim_kubernetes_user + vim_kubernetes_3 = _vim_kubernetes_oidc result_1 = vim_utils.vim_to_conn_info(vim_openstack) self.assertEqual('openstack-1', result_1.vimId) @@ -109,6 +122,9 @@ class TestVimUtils(base.BaseTestCase): result_3 = vim_utils.vim_to_conn_info(vim_kubernetes_2) self.assertEqual('kubernetes-2', result_3.vimId) + result_4 = vim_utils.vim_to_conn_info(vim_kubernetes_3) + self.assertEqual('kubernetes-3', result_4.vimId) + self.assertRaises( sol_ex.SolException, vim_utils.vim_to_conn_info, {'vim_type': 'test', 'vim_auth': 'test'}) diff --git a/tacker/tests/unit/vnfm/test_vim_client.py b/tacker/tests/unit/vnfm/test_vim_client.py index 099c1ed7f..8fd5d870b 100644 --- a/tacker/tests/unit/vnfm/test_vim_client.py +++ b/tacker/tests/unit/vnfm/test_vim_client.py @@ -77,6 +77,35 @@ class TestVIMClient(base.TestCase): 'tenant': 'test', 'extra': {}} self.assertEqual(vim_expect, vim_result) + def test_get_vim_oidc_auth(self): + self.nfvo_plugin.get_vim.return_value = { + 'id': 'aaaa', 'name': 'VIM0', 'type': 'test_vim', + 'auth_cred': {'password': '****', + 'client_secret': '****', + 'ssl_ca_cert': '****'}, + 'auth_url': 'http://127.0.0.1/identity/v3', + 'placement_attr': {'regions': ['TestRegionOne']}, + 'tenant_id': 'test'} + self.service_plugins.get.return_value = self.nfvo_plugin + self.vimclient._build_vim_auth = mock.Mock() + self.vimclient._build_vim_auth.return_value = { + 'password': '****', + 'client_secret': '****', + 'ssl_ca_cert': '****'} + with mock.patch.object(manager.TackerManager, 'get_service_plugins', + return_value=self.service_plugins): + vim_result = self.vimclient.get_vim(None, + vim_id=self.vim_info['id'], + region_name='TestRegionOne') + vim_expect = {'vim_auth': {'password': '****', + 'client_secret': '****', + 'ssl_ca_cert': '****'}, + 'vim_id': 'aaaa', + 'vim_name': 'VIM0', 'vim_type': 'test_vim', + 'placement_attr': {'regions': ['TestRegionOne']}, + 'tenant': 'test', 'extra': {}} + self.assertEqual(vim_expect, vim_result) + def test_get_vim_with_default_name(self): self.vim_info.pop('name') self.nfvo_plugin.get_vim.return_value = self.vim_info diff --git a/tacker/vnfm/vim_client.py b/tacker/vnfm/vim_client.py index a91755804..1dea065a0 100644 --- a/tacker/vnfm/vim_client.py +++ b/tacker/vnfm/vim_client.py @@ -95,6 +95,13 @@ class VimClient(object): vim_auth, vim_auth['ssl_ca_cert']) + # decode client_secret + if 'client_secret' in vim_auth and vim_auth['client_secret']: + vim_auth['client_secret'] = self._decode_vim_auth( + vim_info['id'], + vim_auth, + vim_auth['client_secret']) + vim_auth['auth_url'] = vim_info['auth_url'] # These attributes are needless for authentication diff --git a/tools/test-setup-k8s-vim.sh b/tools/test-setup-k8s-vim.sh index 13973db6e..896b401dd 100755 --- a/tools/test-setup-k8s-vim.sh +++ b/tools/test-setup-k8s-vim.sh @@ -15,13 +15,26 @@ # --os-auth-url # --config-file -openstack vim register \ - --os-username nfv_user \ - --os-project-name nfv \ - --os-password devstack \ - --os-auth-url http://127.0.0.1/identity \ - --os-project-domain-name Default \ - --os-user-domain-name Default \ - --description "Kubernetes VIM" \ - --config-file /opt/stack/tacker/tacker/tests/etc/samples/local-k8s-vim.yaml \ - vim-kubernetes \ No newline at end of file +conf_dir=/opt/stack/tacker/tacker/tests/etc/samples + +register_vim() { + openstack vim register \ + --os-username nfv_user \ + --os-project-name nfv \ + --os-password devstack \ + --os-auth-url http://127.0.0.1/identity \ + --os-project-domain-name Default \ + --os-user-domain-name Default \ + --description "Kubernetes VIM" \ + --config-file $1 \ + $2 +} + +# regiter vim with bearer token +register_vim $conf_dir/local-k8s-vim.yaml vim-kubernetes + +# regiter vim with OpenID Connect info +if [ -f $conf_dir/local-k8s-vim-oidc.yaml ] +then + register_vim $conf_dir/local-k8s-vim-oidc.yaml vim-kubernetes-oidc +fi \ No newline at end of file diff --git a/tox.ini b/tox.ini index 36a90eb5e..184bb1c9f 100644 --- a/tox.ini +++ b/tox.ini @@ -73,6 +73,12 @@ setenv = {[testenv]setenv} commands = stestr --test-path=./tacker/tests/functional/sol_kubernetes_v2 run --slowest --concurrency 1 {posargs} +[testenv:dsvm-functional-sol_kubernetes_oidc_auth] +setenv = {[testenv]setenv} + +commands = + stestr --test-path=./tacker/tests/functional/sol_kubernetes_oidc_auth run --slowest --concurrency 1 {posargs} + [testenv:dsvm-functional-sol-multi-tenant] setenv = {[testenv]setenv}