From: Eduardo Sousa Date: Tue, 31 Jul 2018 00:20:02 +0000 (+0100) Subject: Adding Authentication Connector plugin system X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=cede4eb5c53b8e8ec9189b9770cdad9e07041fd2;p=osm%2FNBI.git Adding Authentication Connector plugin system Change-Id: Iab8cfb3dc72e8d4e0b38a575603c02ab7ffd85de Signed-off-by: Eduardo Sousa --- diff --git a/Dockerfile.local b/Dockerfile.local index 1ec838a..5687caf 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -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"] diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index 572ab88..5dedf56 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -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 " +__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 " - -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 index 0000000..c6c0202 --- /dev/null +++ b/osm_nbi/authconn.py @@ -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 " +__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 index 0000000..5b8cad7 --- /dev/null +++ b/osm_nbi/authconn_keystone.py @@ -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 " +__date__ = "$27-jul-2018 23:59:59$" + +from authconn import Authconn + + +class AuthconnKeystone(Authconn): + def __init__(self, config): + Authconn.__init__(self, config) diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index b59a9e3..921b07c 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -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 " __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)