Add the authentication middleware for service. 32/9332/3
authordliu5 <david.liu@windriver.com>
Sun, 9 Oct 2022 11:02:52 +0000 (19:02 +0800)
committerdliu5 <david.liu@windriver.com>
Thu, 20 Oct 2022 07:25:57 +0000 (15:25 +0800)
Issue-ID: INF-299

Signed-off-by: dliu5 <david.liu@windriver.com>
Change-Id: I60fe9351532986f4c275bd7e4d1513393a373e08

charts/resources/scripts/init/o2api_start.sh
charts/templates/deployment.yaml
charts/templates/serviceaccount.yaml [new file with mode: 0644]
charts/values.yaml
docs/installation-guide.rst
o2app/entrypoints/flask_application.py
o2common/authmw/__init__.py [new file with mode: 0644]
o2common/authmw/authmiddleware.py [new file with mode: 0644]
o2common/authmw/authprov.py [new file with mode: 0644]
o2common/config/config.py
tests/unit/test_watcher.py

index 46ea5f5..65f3cbd 100644 (file)
@@ -23,12 +23,14 @@ git clone "https://gerrit.o-ran-sc.org/r/pti/o2"
 
 pip install -e /root/o2
 
+
 cat <<EOF>>/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
index 9da4fba..5d82063 100644 (file)
@@ -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 (file)
index 0000000..1cae523
--- /dev/null
@@ -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
index c343088..0771680 100644 (file)
@@ -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
index 68f7ed0..9b63c7c 100644 (file)
@@ -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=<INF external_oam_floating_address e.g.: 128.10.10.10>
 
+  # please specify the smo service account yaml file
+  export SMO_SERVICEACCOUNT=<your input here eg.: smo>
+  # service account and binding for smo yaml file
+
+  cat <<EOF >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 <<EOF>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
index fff1201..f74dca2 100644 (file)
@@ -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 (file)
index 0000000..0e1e364
--- /dev/null
@@ -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 (file)
index 0000000..c70adfc
--- /dev/null
@@ -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 (file)
index 0000000..17c5349
--- /dev/null
@@ -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 "<auth_definer: name = %s>" % 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
index cf1c08c..b1d2cae 100644 (file)
@@ -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'
index 4bb707f..5e1960f 100644 (file)
@@ -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