From 5601b5899b0fd15748ae0474de9f5f6dda72864c Mon Sep 17 00:00:00 2001 From: dliu5 Date: Sun, 9 Oct 2022 19:02:52 +0800 Subject: [PATCH] Add the authentication middleware for service. Issue-ID: INF-299 Signed-off-by: dliu5 Change-Id: I60fe9351532986f4c275bd7e4d1513393a373e08 --- charts/resources/scripts/init/o2api_start.sh | 2 + charts/templates/deployment.yaml | 1 + charts/templates/serviceaccount.yaml | 20 ++++ charts/values.yaml | 2 +- docs/installation-guide.rst | 47 ++++++++- o2app/entrypoints/flask_application.py | 25 ++++- o2common/authmw/__init__.py | 13 +++ o2common/authmw/authmiddleware.py | 83 +++++++++++++++ o2common/authmw/authprov.py | 144 +++++++++++++++++++++++++++ o2common/config/config.py | 43 +++++++- tests/unit/test_watcher.py | 4 +- 11 files changed, 377 insertions(+), 7 deletions(-) create mode 100644 charts/templates/serviceaccount.yaml create mode 100644 o2common/authmw/__init__.py create mode 100644 o2common/authmw/authmiddleware.py create mode 100644 o2common/authmw/authprov.py diff --git a/charts/resources/scripts/init/o2api_start.sh b/charts/resources/scripts/init/o2api_start.sh index 46ea5f5..65f3cbd 100644 --- a/charts/resources/scripts/init/o2api_start.sh +++ b/charts/resources/scripts/init/o2api_start.sh @@ -23,12 +23,14 @@ git clone "https://gerrit.o-ran-sc.org/r/pti/o2" pip install -e /root/o2 + cat <>/etc/hosts 127.0.0.1 api 127.0.0.1 postgres 127.0.0.1 redis EOF + flask run --host=0.0.0.0 --port=80 sleep infinity diff --git a/charts/templates/deployment.yaml b/charts/templates/deployment.yaml index 9da4fba..5d82063 100644 --- a/charts/templates/deployment.yaml +++ b/charts/templates/deployment.yaml @@ -30,6 +30,7 @@ spec: labels: app: o2api spec: + serviceAccountName: {{ .Values.o2ims.serviceaccountname }} imagePullSecrets: - name: {{ .Values.o2ims.imagePullSecrets }} {{- if .Values.o2ims.affinity }} diff --git a/charts/templates/serviceaccount.yaml b/charts/templates/serviceaccount.yaml new file mode 100644 index 0000000..1cae523 --- /dev/null +++ b/charts/templates/serviceaccount.yaml @@ -0,0 +1,20 @@ +# Service Account for o2ims +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.o2ims.serviceaccountname }} + namespace: orano2 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Values.o2ims.serviceaccountname }} +subjects: +- kind: ServiceAccount + namespace: orano2 + name: {{ .Values.o2ims.serviceaccountname }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin diff --git a/charts/values.yaml b/charts/values.yaml index c343088..0771680 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -30,7 +30,7 @@ global: namespace: orano2 o2ims: - imagePullSecrets: admin-orano2-registry-secret + serviceaccountname: admin image: repository: registry.local:9001/admin/o2imsdms tag: 0.1.1 diff --git a/docs/installation-guide.rst b/docs/installation-guide.rst index 68f7ed0..9b63c7c 100644 --- a/docs/installation-guide.rst +++ b/docs/installation-guide.rst @@ -139,6 +139,48 @@ The following instruction should be done outside of INF platform controller host # export API_HOST_EXTERNAL_FLOATING=$(echo ${OS_AUTH_URL} | sed -e s,`echo ${OS_AUTH_URL} | grep :// | sed -e's,^\(.*//\).*,\1,g'`,,g | cut -d/ -f1 | sed -e 's,:.*,,g') export API_HOST_EXTERNAL_FLOATING= + # please specify the smo service account yaml file + export SMO_SERVICEACCOUNT= + # service account and binding for smo yaml file + + cat <smo-serviceaccount.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + namespace: default + name: pod-reader + rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["pods"] + verbs: ["get", "watch", "list"] + --- + apiVersion: v1 + kind: ServiceAccount + metadata: + name: ${SMO_SERVICEACCOUNT} + namespace: default + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: read-pods + namespace: default + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: pod-reader + subjects: + - kind: ServiceAccount + name: ${SMO_SERVICEACCOUNT} + namespace: default + + EOF + + kubectl apply -f smo-serviceaccount.yaml + + #export the smo account token data + export SMO_TOKEN_DATA=$(kubectl -n default describe secret $(kubectl -n default get secret | grep ${SMO_SERVICEACCOUNT} | awk '{print $1}') | grep "token:" | awk '{print $2}') + cat <o2service-override.yaml o2ims: imagePullSecrets: admin-orano2-registry-secret @@ -154,6 +196,7 @@ The following instruction should be done outside of INF platform controller host OS_PASSWORD: "${OS_PASSWORD}" K8S_KUBECONFIG: "/opt/k8s_kube.conf" API_HOST_EXTERNAL_FLOATING: "${API_HOST_EXTERNAL_FLOATING}" + EOF @@ -164,8 +207,8 @@ The following instruction should be done outside of INF platform controller host helm install o2service o2/charts/ -f o2service-override.yaml helm list |grep o2service - kubectl -n ${NAMESPACE} get pods |grep o2service - kubectl -n ${NAMESPACE} get services |grep o2service + kubectl -n ${NAMESPACE} get pods |grep o2api + kubectl -n ${NAMESPACE} get services |grep o2api 2.4 Verify INF O2 service diff --git a/o2app/entrypoints/flask_application.py b/o2app/entrypoints/flask_application.py index fff1201..f74dca2 100644 --- a/o2app/entrypoints/flask_application.py +++ b/o2app/entrypoints/flask_application.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 Wind River Systems, Inc. +# Copyright (C) 2021-2022 Wind River Systems, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,9 +21,32 @@ from o2ims.views import configure_namespace as ims_route_configure_namespace from o2dms.api import configure_namespace as dms_route_configure_namespace from o2ims.adapter.clients.alarm_dict_client import load_alarm_definition +from o2common.authmw import authmiddleware +from o2common.authmw import authprov +from o2common.config.config import get_review_url +from o2common.helper import o2logging # apibase = config.get_o2ims_api_base() +auth = True app = Flask(__name__) +logger = o2logging.get_logger(__name__) + + +def _get_k8s_url(): + try: + token_review_url = get_review_url() + return token_review_url + except Exception: + raise Exception('Get k8s token review url failed') + + +if auth: + # perform service account identity&privilege check. + _get_k8s_url() + ad = authprov.auth_definer('ad') + ad.sanity_check() + app.wsgi_app = authmiddleware.authmiddleware(app.wsgi_app) + app.config.SWAGGER_UI_DOC_EXPANSION = 'list' api = Api(app, version='1.0.0', title='INF O2 Services API', diff --git a/o2common/authmw/__init__.py b/o2common/authmw/__init__.py new file mode 100644 index 0000000..0e1e364 --- /dev/null +++ b/o2common/authmw/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2022 Wind River Systems, Inc. +# +# 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. diff --git a/o2common/authmw/authmiddleware.py b/o2common/authmw/authmiddleware.py new file mode 100644 index 0000000..c70adfc --- /dev/null +++ b/o2common/authmw/authmiddleware.py @@ -0,0 +1,83 @@ +# Copyright (C) 2022 Wind River Systems, Inc. +# +# 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 werkzeug.wrappers import Request, Response +from o2common.helper import o2logging +from o2common.authmw.authprov import auth_definer + +logger = o2logging.get_logger(__name__) + + +class AuthRequiredExp(Exception): + def __init__(self, value): + self.value = value + + def dictize(self): + return { + 'WWW-Authenticate': '{}'.format(self.value)} + + +class AuthFailureExp(Exception): + def __init__(self, value): + self.value = value + + def dictize(self): + return { + 'WWW-Authenticate': '{}'.format(self.value)} + + +def _response_wrapper(environ, start_response, header): + res = Response(headers=header, + mimetype='text/plain', status=401) + return res(environ, start_response) + + +class authmiddleware(): + + ''' + Auth WSGI middleware + ''' + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + logger.info(__name__ + 'authentication middleware') + req = Request(environ, populate_request=True, shallow=True) + try: + auth_header = req.headers['Authorization'] + + if auth_header: + auth_token = auth_header.split(" ")[1] + + ad = auth_definer('oauth') + # invoke underlying auth mdw to make k8s/keystone api + ret = ad.authenticate(auth_token) + if ret is True: + logger.info( + "auth success with oauth token: " + auth_token) + return self.app(environ, start_response) + else: + raise AuthFailureExp( + 'Bearer realm="Authentication Failed"') + else: + raise AuthRequiredExp('Bearer realm="Authentication Required"') + except AuthRequiredExp as ex: + return _response_wrapper(environ, start_response, ex.dictize()) + except AuthFailureExp as ex: + return _response_wrapper(environ, start_response, ex.dictize()) + except Exception: + hint = 'Bearer realm="Authentication Required"' + return _response_wrapper(environ, start_response, + AuthRequiredExp(hint).dictize()) diff --git a/o2common/authmw/authprov.py b/o2common/authmw/authprov.py new file mode 100644 index 0000000..17c5349 --- /dev/null +++ b/o2common/authmw/authprov.py @@ -0,0 +1,144 @@ +# Copyright (C) 2022 Wind River Systems, Inc. +# +# 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 ssl +from o2common.helper import o2logging +import urllib.request +import urllib.parse +import json + +from o2common.config.config import get_auth_provider, get_review_url +from o2common.config.config import get_reviewer_token + +ssl._create_default_https_context = ssl._create_unverified_context +logger = o2logging.get_logger(__name__) + +# read the conf from config file +auth_prv_conf = get_auth_provider() + +try: + token_review_url = get_review_url() +except Exception: + raise Exception('Get k8s token review url failed') + + +class K8SAuthenticaException(Exception): + def __init__(self, value): + self.value = value + + +class K8SAuthorizationException(Exception): + def __init__(self, value): + self.value = value + + +class auth_definer(): + + def __init__(self, name): + super().__init__() + self.name = name + if auth_prv_conf == 'k8s': + self.obj = k8s_auth_provider('k8s') + else: + self.obj = keystone_auth_provider('keystone') + + def tokenissue(self): + return self.obj.tokenissue() + + def sanity_check(self): + return self.obj.sanity_check() + + # call k8s api + def authenticate(self, token): + return self.obj.authenticate(token) + + def __repr__(self) -> str: + return "" % self.name + + +class k8s_auth_provider(auth_definer): + + def __init__(self, name): + self.name = name + + def tokenissue(self, **args2): + pass + + def sanity_check(self): + try: + self.authenticate('faketoken') + except Exception as ex: + logger.critical( + 'Failed to bootstrap oauth middleware with exp: ' + str(ex)) + raise Exception(str(ex)) + + def authenticate(self, token): + reviewer_token = get_reviewer_token() + tokenreview = { + "kind": "TokenReview", + "apiVersion": "authentication.k8s.io/v1", + "metadata": { + "creationTimestamp": None + }, + "spec": { + "token": ""+token + }, + "status": { + "user": {} + } + } + datas = json.dumps(tokenreview) + binary_data = datas.encode('utf-8') + # 'post' method + header = {'Authorization': 'Bearer '+reviewer_token, + 'Content-Type': 'application/json'} + try: + req = urllib.request.Request( + token_review_url, data=binary_data, headers=header) + response = urllib.request.urlopen(req) + data = json.load(response) + if data['status']['authenticated'] is True: + logger.info("Authenticated.") + return True + except Exception as ex: + strex = str(ex) + logger.warning( + "Invoke K8s API Service Exception happened:" + strex) + if '403' in strex: + raise K8SAuthorizationException( + 'No privilege to perform oauth token check.') + elif '401' in strex: + raise K8SAuthenticaException( + 'Self Authentication failure.') + return False + + def tokenrevoke(self, **args2): + return True + + +class keystone_auth_provider(auth_definer): + def __init__(self, name): + self.name = name + + def tokenissue(self, *args1, **args2): + pass + + def authenticate(self, *args1, **args2): + return False + + def sanity_check(self): + pass + + def tokenrevoke(self, *args1, **args2): + return False diff --git a/o2common/config/config.py b/o2common/config/config.py index cf1c08c..b1d2cae 100644 --- a/o2common/config/config.py +++ b/o2common/config/config.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 Wind River Systems, Inc. +# Copyright (C) 2021-2022 Wind River Systems, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -270,3 +270,44 @@ def get_events_yaml_filename(): if events_yaml_name is not None and os.path.isfile(events_yaml_name): return events_yaml_name return "/configs/events.yaml" + +# get k8s host from env: + + +def get_k8s_host(): + k8s_host = os.environ.get("KUBERNETES_SERVICE_HOST") + if k8s_host is None: + raise Exception('Get k8s host failed.') + return k8s_host + +# get k8s host port from env: + + +def get_k8s_port(): + k8s_port = os.environ.get("KUBERNETES_SERVICE_PORT_HTTPS", '443') + return k8s_port + +# token review url + + +def get_review_url(): + try: + api = '/apis/authentication.k8s.io/v1/tokenreviews' + return "{0}{1}:{2}{3}".format( + 'https://', get_k8s_host(), get_k8s_port(), api) + except Exception: + raise Exception('Get k8s review url failed') + +# get reviewer token + + +def get_reviewer_token(): + # token path default is below. + token_path = '/var/run/secrets/kubernetes.io/serviceaccount/token' + with open(token_path, 'r') as f: + ctt = f.read() + return ctt + + +def get_auth_provider(): + return 'k8s' diff --git a/tests/unit/test_watcher.py b/tests/unit/test_watcher.py index 4bb707f..5e1960f 100644 --- a/tests/unit/test_watcher.py +++ b/tests/unit/test_watcher.py @@ -195,10 +195,10 @@ def test_watchers_worker(): count1 = fakewatcher.fakeOcloudWatcherCounter testedworker.start() - time.sleep(20) + time.sleep(1) assert fakewatcher.fakeOcloudWatcherCounter > count1 # assumed hacking: probe has stopped the sched task count3 = fakewatcher.fakeOcloudWatcherCounter - time.sleep(3) + time.sleep(1) assert fakewatcher.fakeOcloudWatcherCounter == count3 -- 2.16.6