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:
parent
dbca617b98
commit
57902730d6
14
.zuul.yaml
14
.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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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:
|
||||
|
1
roles/setup-k8s-oidc/defaults/main.yaml
Normal file
1
roles/setup-k8s-oidc/defaults/main.yaml
Normal file
@ -0,0 +1 @@
|
||||
oidc_work_dir: "/tmp/oidc"
|
11
roles/setup-k8s-oidc/files/cluster_role_binding.yaml
Normal file
11
roles/setup-k8s-oidc/files/cluster_role_binding.yaml
Normal 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
|
10
roles/setup-k8s-oidc/files/create_keycloak.sh
Normal file
10
roles/setup-k8s-oidc/files/create_keycloak.sh
Normal 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
|
27
roles/setup-k8s-oidc/files/generate_ssl_cert.sh
Normal file
27
roles/setup-k8s-oidc/files/generate_ssl_cert.sh
Normal 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 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
|
19
roles/setup-k8s-oidc/files/import_oidc_realm.sh
Normal file
19
roles/setup-k8s-oidc/files/import_oidc_realm.sh
Normal 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"
|
1843
roles/setup-k8s-oidc/files/oidc_realm.json
Normal file
1843
roles/setup-k8s-oidc/files/oidc_realm.json
Normal file
File diff suppressed because it is too large
Load Diff
12
roles/setup-k8s-oidc/files/ssl_csr.conf
Normal file
12
roles/setup-k8s-oidc/files/ssl_csr.conf
Normal 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
|
109
roles/setup-k8s-oidc/tasks/main.yaml
Normal file
109
roles/setup-k8s-oidc/tasks/main.yaml
Normal 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
|
||||
|
@ -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
|
||||
|
57
tacker/common/oidc_utils.py
Normal file
57
tacker/common/oidc_utils.py
Normal 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))
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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'] =\
|
||||
|
@ -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")
|
||||
|
57
tacker/sol_refactored/common/oidc_utils.py
Normal file
57
tacker/sol_refactored/common/oidc_utils.py
Normal 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))
|
@ -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']
|
||||
|
@ -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:
|
||||
|
9
tacker/tests/etc/samples/local-k8s-vim-oidc.yaml
Normal file
9
tacker/tests/etc/samples/local-k8s-vim-oidc.yaml
Normal 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"
|
147
tacker/tests/functional/sol_kubernetes_oidc_auth/test_vim.py
Normal file
147
tacker/tests/functional/sol_kubernetes_oidc_auth/test_vim.py
Normal 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)
|
@ -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'])
|
@ -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
|
@ -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)
|
@ -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.
|
||||
|
0
tacker/tests/unit/common/container/__init__.py
Normal file
0
tacker/tests/unit/common/container/__init__.py
Normal 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)
|
||||
|
123
tacker/tests/unit/common/test_oidc_utils.py
Normal file
123
tacker/tests/unit/common/test_oidc_utils.py
Normal 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())
|
@ -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(
|
||||
|
123
tacker/tests/unit/sol_refactored/common/test_oidc_utils.py
Normal file
123
tacker/tests/unit/sol_refactored/common/test_oidc_utils.py
Normal 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})
|
@ -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'})
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
6
tox.ini
6
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}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user