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}