Adding Authentication Connector plugin system 89/6389/1
authorEduardo Sousa <eduardosousa@av.it.pt>
Tue, 31 Jul 2018 00:20:02 +0000 (01:20 +0100)
committerEduardo Sousa <eduardosousa@av.it.pt>
Tue, 31 Jul 2018 00:20:02 +0000 (01:20 +0100)
Change-Id: Iab8cfb3dc72e8d4e0b38a575603c02ab7ffd85de
Signed-off-by: Eduardo Sousa <eduardosousa@av.it.pt>
Dockerfile.local
osm_nbi/auth.py
osm_nbi/authconn.py [new file with mode: 0644]
osm_nbi/authconn_keystone.py [new file with mode: 0644]
osm_nbi/nbi.py

index 1ec838a..5687caf 100644 (file)
@@ -43,26 +43,39 @@ VOLUME /app/log
 
 # The following ENV can be added with "docker run -e xxx' to configure
 # server
-ENV OSMNBI_SOCKET_HOST     0.0.0.0
-ENV OSMNBI_SOCKET_PORT     9999
+ENV OSMNBI_SOCKET_HOST                          0.0.0.0
+ENV OSMNBI_SOCKET_PORT                          9999
 # storage
-ENV OSMNBI_STORAGE_PATH    /app/storage
+ENV OSMNBI_STORAGE_PATH                         /app/storage
 # database
-ENV OSMNBI_DATABASE_DRIVER mongo
-ENV OSMNBI_DATABASE_HOST   mongo
-ENV OSMNBI_DATABASE_PORT   27017
+ENV OSMNBI_DATABASE_DRIVER                      mongo
+ENV OSMNBI_DATABASE_HOST                        mongo
+ENV OSMNBI_DATABASE_PORT                        27017
 # web
-ENV OSMNBI_STATIC_DIR      /app/osm_nbi/html_public
+ENV OSMNBI_STATIC_DIR                           /app/osm_nbi/html_public
 # logs
-ENV OSMNBI_LOG_FILE        /app/log
-ENV OSMNBI_LOG_LEVEL       DEBUG
+ENV OSMNBI_LOG_FILE                             /app/log
+ENV OSMNBI_LOG_LEVEL                            DEBUG
 # message
-ENV OSMNBI_MESSAGE_DRIVER  kafka
-ENV OSMNBI_MESSAGE_HOST    kafka
-ENV OSMNBI_MESSAGE_PORT    9092
+ENV OSMNBI_MESSAGE_DRIVER                       kafka
+ENV OSMNBI_MESSAGE_HOST                         kafka
+ENV OSMNBI_MESSAGE_PORT                         9092
 # logs
-ENV OSMNBI_LOG_FILE        /app/log/nbi.log
-ENV OSMNBI_LOG_LEVEL       DEBUG
+ENV OSMNBI_LOG_FILE                             /app/log/nbi.log
+ENV OSMNBI_LOG_LEVEL                            DEBUG
+# authenticator
+ENV OSMNBI_AUTHENTICATOR_DB_HOST                mysql
+ENV OSMNBI_AUTHENTICATOR_DB_PORT                3306
+ENV OSMNBI_AUTHENTICATOR_DB_ROOT_PASSWORD       mysql
+ENV OSMNBI_AUTHENTICATOR_DB_USER                mysql
+ENV OSMNBI_AUTHENTICATOR_DB_PASS                mysql
+ENV OSMNBI_AUTHENTICATOR_BACKEND                keystone
+ENV OSMNBI_AUTHENTICATOR_AUTH_URL               keystone
+ENV OSMNBI_AUTHENTICATOR_USER_DOMAIN_NAME       keystone
+ENV OSMNBI_AUTHENTICATOR_PROJECT_DOMAIN_NAME    keystone
+ENV OSMNBI_AUTHENTICATOR_SERVICE_USERNAME       authenticator
+ENV OSMNBI_AUTHENTICATOR_SERVICE_PASSWORD       authenticator
+ENV OSMNBI_AUTHENTICATOR_SERVICE_PROJECT        admin
 
 # Run app.py when the container launches
 CMD ["python3", "nbi.py"]
index 572ab88..5dedf56 100644 (file)
@@ -1,25 +1,89 @@
+# -*- coding: utf-8 -*-
+
+"""
+Authenticator is responsible for authenticating the users,
+create the tokens unscoped and scoped, retrieve the role
+list inside the projects that they are inserted
+"""
+
+__author__ = "Eduardo Sousa <eduardosousa@av.it.pt>"
+__date__ = "$27-jul-2018 23:59:59$"
+
+import logging
+
 import cherrypy
 from base64 import standard_b64decode
 from http import HTTPStatus
 
-
+from authconn_keystone import AuthconnKeystone
 from engine import EngineException
 
-__author__ = "Eduardo Sousa <eduardosousa@av.it.pt>"
-
 
-class AuthenticatorException(Exception):
+class AuthException(Exception):
     def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED):
         self.http_code = http_code
         Exception.__init__(self, message)
 
 
-class Authenticator(object):
+class Authenticator:
+    """
+    This class should hold all the mechanisms for User Authentication and
+    Authorization. Initially it should support Openstack Keystone as a
+    backend through a plugin model where more backends can be added and a
+    RBAC model to manage permissions on operations.
+    """
+
     def __init__(self, engine):
+        """
+        Authenticator initializer. Setup the initial state of the object,
+        while it waits for the config dictionary and database initialization.
+
+        Note: engine is only here until all the calls can to it can be replaced.
+
+        :param engine: reference to engine object used.
+        """
         super().__init__()
 
         self.engine = engine
 
+        self.backend = None
+        self.config = None
+        self.db = None
+        self.logger = logging.getLogger("nbi.authenticator")
+
+    def start(self, config):
+        """
+        Method to configure the Authenticator object. This method should be called
+        after object creation. It is responsible by initializing the selected backend,
+        as well as the initialization of the database connection.
+
+        :param config: dictionary containing the relevant parameters for this object.
+        """
+        self.config = config
+
+        try:
+            if not self.backend:
+                if config["authenticator"]["backend"] == "keystone":
+                    self.backend = AuthconnKeystone(self.config["authenticator"])
+            if not self.db:
+                pass
+                # TODO: Implement database initialization
+                # NOTE: Database needed to store the mappings
+        except Exception as e:
+            raise AuthException(str(e))
+
+        pass
+
+    def init_db(self, target_version='1.0'):
+        """
+        Check if the database has been initialized. If not, create the required tables
+        and insert the predefined mappings between roles and permissions.
+
+        :param target_version: schema version that should be present in the database.
+        :return: None if OK, exception if error or version is different.
+        """
+        pass
+
     def authorize(self):
         token = None
         user_passwd64 = None
@@ -60,7 +124,7 @@ class Authenticator(object):
             if cherrypy.session.get('Authorization'):
                 del cherrypy.session['Authorization']
             cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
-            raise AuthenticatorException(str(e))
+            raise AuthException(str(e))
 
     def new_token(self, session, indata, remote):
         return self.engine.new_token(session, indata, remote)
diff --git a/osm_nbi/authconn.py b/osm_nbi/authconn.py
new file mode 100644 (file)
index 0000000..c6c0202
--- /dev/null
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+
+"""
+Authconn implements an Abstract class for the Auth backend connector
+plugins with the definition of the methods to be implemented.
+"""
+
+__author__ = "Eduardo Sousa <eduardosousa@av.it.pt>"
+__date__ = "$27-jul-2018 23:59:59$"
+
+from http import HTTPStatus
+
+
+class AuthconnException(Exception):
+    """
+    Common and base class Exception for all authconn exceptions.
+    """
+    def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED):
+        Exception.__init__(message)
+        self.http_code = http_code
+
+
+class AuthconnConnectionException(AuthconnException):
+    """
+    Connectivity error with Auth backend.
+    """
+    def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED):
+        AuthconnException.__init__(self, message, http_code)
+
+
+class AuthconnNotSupportedException(AuthconnException):
+    """
+    The request is not supported by the Auth backend.
+    """
+    def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED):
+        AuthconnException.__init__(self, message, http_code)
+
+
+class AuthconnNotImplementedException(AuthconnException):
+    """
+    The method is not implemented by the Auth backend.
+    """
+    def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED):
+        AuthconnException.__init__(self, message, http_code)
+
+
+class Authconn:
+    """
+    Abstract base class for all the Auth backend connector plugins.
+    Each Auth backend connector plugin must be a subclass of
+    Authconn class.
+    """
+    def __init__(self, config):
+        """
+        Constructor of the Authconn class.
+
+        Note: each subclass
+
+        :param config: configuration dictionary containing all the
+        necessary configuration parameters.
+        """
+        self.config = config
+
+    def authenticate_with_user_password(self, user, password):
+        """
+        Authenticate a user using username and password.
+
+        :param user: username
+        :param password: password
+        :return: an unscoped token that grants access to project list
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def authenticate_with_token(self, token, project=None):
+        """
+        Authenticate a user using a token. Can be used to revalidate the token
+        or to get a scoped token.
+
+        :param token: a valid token.
+        :param project: (optional) project for a scoped token.
+        :return: return a revalidated token, scoped if a project was passed or
+        the previous token was already scoped.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def validate_token(self, token):
+        """
+        Check if the token is valid.
+
+        :param token: token to validate
+        :return: dictionary with information associated with the token. If the
+        token is not valid, returns None.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def revoke_token(self, token):
+        """
+        Invalidate a token.
+
+        :param token: token to be revoked
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def get_project_list(self, token):
+        """
+        Get all the projects associated with a user.
+
+        :param token: valid token
+        :return: list of projects
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def get_role_list(self, token):
+        """
+        Get role list for a scoped project.
+
+        :param token: scoped token.
+        :return: returns the list of roles for the user in that project. If
+        the token is unscoped it returns None.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def create_user(self, user, password):
+        """
+        Create a user.
+
+        :param user: username.
+        :param password: password.
+        :return: boolean to indicate if operation was successful.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def change_password(self, user, old_password, new_password):
+        """
+        Change the user password.
+
+        :param user: username.
+        :param old_password: old password.
+        :param new_password: new password.
+        :return: boolean to indicate if operation was successful.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def delete_user(self, user):
+        """
+        Delete user.
+
+        :param user: username.
+        :return: boolean to indicate if operation was successful.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def create_role(self, role):
+        """
+        Create a role.
+
+        :param role: role name.
+        :return: boolean to indicate if operation was successful.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def delete_role(self, role):
+        """
+        Delete a role.
+
+        :param role: role name.
+        :return: boolean to indicate if operation was successful.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def create_project(self, project):
+        """
+        Create a project.
+
+        :param project: project name.
+        :return: boolean to indicate if operation was successful.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def delete_project(self, project):
+        """
+        Delete a project.
+
+        :param project: project name.
+        :return: boolean to indicate if operation was successful.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
+    def assign_role_to_user(self, user, project, role):
+        """
+        Assigning a role to a user in a project.
+
+        :param user: username.
+        :param project: project name.
+        :param role: role name.
+        :return: boolean to indicate if operation was successful.
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
diff --git a/osm_nbi/authconn_keystone.py b/osm_nbi/authconn_keystone.py
new file mode 100644 (file)
index 0000000..5b8cad7
--- /dev/null
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+
+"""
+AuthconnKeystone implements implements the connector for
+Openstack Keystone and leverages the RBAC model, to bring
+it for OSM.
+"""
+
+__author__ = "Eduardo Sousa <eduardosousa@av.it.pt>"
+__date__ = "$27-jul-2018 23:59:59$"
+
+from authconn import Authconn
+
+
+class AuthconnKeystone(Authconn):
+    def __init__(self, config):
+        Authconn.__init__(self, config)
index b59a9e3..921b07c 100644 (file)
@@ -11,7 +11,7 @@ import logging.handlers
 import getopt
 import sys
 
-from auth import Authenticator
+from auth import Authenticator, AuthException
 from engine import Engine, EngineException
 from osm_common.dbbase import DbException
 from osm_common.fsbase import FsException
@@ -26,6 +26,7 @@ __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 __version__ = "0.1.3"
 version_date = "Apr 2018"
 database_version = '1.0'
+auth_database_version = '1.0'
 
 """
 North Bound Interface  (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
@@ -764,12 +765,13 @@ def _start_service():
                 update_dict['server.socket_host'] = v
             elif k1 in ("server", "test", "auth", "log"):
                 update_dict[k1 + '.' + k2] = v
-            elif k1 in ("message", "database", "storage"):
+            elif k1 in ("message", "database", "storage", "authenticator"):
                 # k2 = k2.replace('_', '.')
-                if k2 == "port":
+                if k2 in ("port", "db_port"):
                     engine_config[k1][k2] = int(v)
                 else:
                     engine_config[k1][k2] = v
+
         except ValueError as e:
             cherrypy.log.error("Ignoring environ '{}': " + str(e))
         except Exception as e:
@@ -821,9 +823,11 @@ def _start_service():
             logger_module.setLevel(engine_config[k1]["loglevel"])
     # TODO add more entries, e.g.: storage
     cherrypy.tree.apps['/osm'].root.engine.start(engine_config)
+    cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
     try:
         cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
-    except EngineException:
+        cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
+    except (EngineException, AuthException):
         pass
     # getenv('OSMOPENMANO_TENANT', None)