Add OpenID Connect Token Auth for k8s

This patch adds openid token auth support when calling k8s APIs.

Openid token auth of k8s relies on an external openid provider,
and Keycloak acts as the openid provider in this implementation.

Implements: blueprint support-openid-k8s-vim
Change-Id: Ie5e080a20cba3ba0ed514ede7955eb16729d797c
This commit is contained in:
Qibin Yao 2022-08-01 16:32:34 +09:00 committed by Ayumu Ueha
parent dbca617b98
commit 57902730d6
46 changed files with 4133 additions and 51 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -0,0 +1 @@
oidc_work_dir: "/tmp/oidc"

View File

@ -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

View File

@ -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

View File

@ -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 Kubernetess 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

View File

@ -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"

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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(

View File

@ -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

View File

@ -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:

View File

@ -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'] =\

View File

@ -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")

View File

@ -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))

View File

@ -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']

View File

@ -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:

View File

@ -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"

View File

@ -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)

View File

@ -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'])

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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())

View File

@ -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(

View File

@ -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})

View File

@ -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'})

View File

@ -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

View File

@ -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

View File

@ -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
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

View File

@ -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}