X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fnbi.py;h=ecd6a5ad30bb9c008fb5de85b92eb8b3c10c427c;hp=afc65c011861bf5421218cba48c1fe01d7628386;hb=341ac1bac7b115d64a50ec166aa5e6d186b39443;hpb=a6bb45d79abef42b8585cfa61aedf32e4326f2c9 diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index afc65c0..ecd6a5a 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -18,34 +18,35 @@ import cherrypy import time import json import yaml -import html_out as html +import osm_nbi.html_out as html import logging import logging.handlers import getopt import sys -from authconn import AuthException -from auth import Authenticator -from engine import Engine, EngineException -from subscriptions import SubscriptionThread -from validation import ValidationError +from osm_nbi.authconn import AuthException, AuthconnException +from osm_nbi.auth import Authenticator +from osm_nbi.engine import Engine, EngineException +from osm_nbi.subscriptions import SubscriptionThread +from osm_nbi.validation import ValidationError from osm_common.dbbase import DbException from osm_common.fsbase import FsException from osm_common.msgbase import MsgException from http import HTTPStatus from codecs import getreader from os import environ, path +from osm_nbi import version as nbi_version, version_date as nbi_version_date __author__ = "Alfonso Tierno " -__version__ = "0.1.3" -version_date = "Jan 2019" +__version__ = "0.1.3" # file version, not NBI version +version_date = "Aug 2019" + database_version = '1.2' auth_database_version = '1.0' nbi_server = None # instance of Server class subscription_thread = None # instance of SubscriptionThread class - """ North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented) URL: /osm GET POST PUT DELETE PATCH @@ -110,6 +111,12 @@ URL: /osm GET POST / O O O /sdns O O / O O O + /k8sclusters O O + / O O O + /k8srepos O O + / O O + /osmrepos O O + / O O /nst/v1 O O /netslice_templates_content O O @@ -190,170 +197,317 @@ Header field name Reference Example Descriptions Retry-After IETF RFC 7231 [19] Fri, 31 Dec 1999 23:59:59 GMT """ - -class NbiException(Exception): - - def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED): - Exception.__init__(self, message) - self.http_code = http_code - - -class Server(object): - instance = 0 - # to decode bytes to str - reader = getreader("utf-8") - - def __init__(self): - self.instance += 1 - self.engine = Engine() - self.authenticator = Authenticator() - self.valid_methods = { # contains allowed URL and methods - "admin": { - "v1": { - "tokens": {"METHODS": ("GET", "POST", "DELETE"), - "": {"METHODS": ("GET", "DELETE")} - }, - "users": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")} - }, - "projects": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE", "PUT")} - }, - "roles": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "POST", "DELETE", "PUT")} - }, - "vims": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")} +valid_query_string = ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC") +# ^ Contains possible administrative query string words: +# ADMIN=True(by default)|Project|Project-list: See all elements, or elements of a project +# (not owned by my session project). +# PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public +# FORCE=True(by default)|False: Force edition/deletion operations +# SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio + +valid_url_methods = { + # contains allowed URL and methods, and the role_permission name + "admin": { + "v1": { + "tokens": {"METHODS": ("GET", "POST", "DELETE"), + "ROLE_PERMISSION": "tokens:", + "": {"METHODS": ("GET", "DELETE"), + "ROLE_PERMISSION": "tokens:id:" + } + }, + "users": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "users:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "users:id:" + } + }, + "projects": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "projects:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "projects:id:"} + }, + "roles": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "roles:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "roles:id:" + } + }, + "vims": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "vims:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "vims:id:" + } + }, + "vim_accounts": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "vim_accounts:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "vim_accounts:id:" + } }, - "vim_accounts": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")} - }, - "wim_accounts": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")} - }, - "sdns": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")} + "wim_accounts": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "wim_accounts:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "wim_accounts:id:" + } }, - } - }, - "pdu": { - "v1": { - "pdu_descriptors": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")} - }, - } - }, - "nsd": { - "v1": { - "ns_descriptors_content": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "PUT", "DELETE")} - }, - "ns_descriptors": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE", "PATCH"), - "nsd_content": {"METHODS": ("GET", "PUT")}, - "nsd": {"METHODS": "GET"}, # descriptor inside package - "artifacts": {"*": {"METHODS": "GET"}} + "sdns": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "sdn_controllers:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "sdn_controllers:id:" + } + }, + "k8sclusters": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "k8sclusters:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "k8sclusters:id:" + } + }, + "k8srepos": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "k8srepos:", + "": {"METHODS": ("GET", "DELETE"), + "ROLE_PERMISSION": "k8srepos:id:" + } + }, + "osmrepos": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "osmrepos:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "osmrepos:id:" + } + }, + "domains": {"METHODS": ("GET", ), + "ROLE_PERMISSION": "domains:", + }, + } + }, + "pdu": { + "v1": { + "pdu_descriptors": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "pduds:", + "": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT"), + "ROLE_PERMISSION": "pduds:id:" + } + }, + } + }, + "nsd": { + "v1": { + "ns_descriptors_content": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "nsds:", + "": {"METHODS": ("GET", "PUT", "DELETE"), + "ROLE_PERMISSION": "nsds:id:" } }, - "pnf_descriptors": {"TODO": ("GET", "POST"), - "": {"TODO": ("GET", "DELETE", "PATCH"), - "pnfd_content": {"TODO": ("GET", "PUT")} - } - }, - "subscriptions": {"TODO": ("GET", "POST"), - "": {"TODO": ("GET", "DELETE")} - }, - } - }, - "vnfpkgm": { - "v1": { - "vnf_packages_content": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "PUT", "DELETE")} - }, - "vnf_packages": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo - "package_content": {"METHODS": ("GET", "PUT"), # package - "upload_from_uri": {"TODO": "POST"} - }, - "vnfd": {"METHODS": "GET"}, # descriptor inside package - "artifacts": {"*": {"METHODS": "GET"}} - } + "ns_descriptors": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "nsds:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), + "ROLE_PERMISSION": "nsds:id:", + "nsd_content": {"METHODS": ("GET", "PUT"), + "ROLE_PERMISSION": "nsds:id:content:", + }, + "nsd": {"METHODS": ("GET",), # descriptor inside package + "ROLE_PERMISSION": "nsds:id:content:" + }, + "artifacts": {"METHODS": ("GET",), + "ROLE_PERMISSION": "nsds:id:nsd_artifact:", + "*": None, + } + } + }, + "pnf_descriptors": {"TODO": ("GET", "POST"), + "": {"TODO": ("GET", "DELETE", "PATCH"), + "pnfd_content": {"TODO": ("GET", "PUT")} + } + }, + "subscriptions": {"TODO": ("GET", "POST"), + "": {"TODO": ("GET", "DELETE")} + }, + } + }, + "vnfpkgm": { + "v1": { + "vnf_packages_content": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "vnfds:", + "": {"METHODS": ("GET", "PUT", "DELETE"), + "ROLE_PERMISSION": "vnfds:id:"} }, - "subscriptions": {"TODO": ("GET", "POST"), - "": {"TODO": ("GET", "DELETE")} - }, - } - }, - "nslcm": { - "v1": { - "ns_instances_content": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE")} - }, - "ns_instances": {"METHODS": ("GET", "POST"), + "vnf_packages": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "vnfds:", + "": {"METHODS": ("GET", "DELETE", "PATCH"), # GET: vnfPkgInfo + "ROLE_PERMISSION": "vnfds:id:", + "package_content": {"METHODS": ("GET", "PUT"), # package + "ROLE_PERMISSION": "vnfds:id:", + "upload_from_uri": {"METHODS": (), + "TODO": ("POST", ), + "ROLE_PERMISSION": "vnfds:id:upload:" + } + }, + "vnfd": {"METHODS": ("GET", ), # descriptor inside package + "ROLE_PERMISSION": "vnfds:id:content:" + }, + "artifacts": {"METHODS": ("GET", ), + "ROLE_PERMISSION": "vnfds:id:vnfd_artifact:", + "*": None, + }, + "action": {"METHODS": ("POST", ), + "ROLE_PERMISSION": "vnfds:id:action:" + }, + } + }, + "subscriptions": {"TODO": ("GET", "POST"), + "": {"TODO": ("GET", "DELETE")} + }, + "vnfpkg_op_occs": {"METHODS": ("GET", ), + "ROLE_PERMISSION": "vnfds:vnfpkgops:", + "": {"METHODS": ("GET", ), + "ROLE_PERMISSION": "vnfds:vnfpkgops:id:" + } + }, + } + }, + "nslcm": { + "v1": { + "ns_instances_content": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "ns_instances:", "": {"METHODS": ("GET", "DELETE"), - "scale": {"METHODS": "POST"}, - "terminate": {"METHODS": "POST"}, - "instantiate": {"METHODS": "POST"}, - "action": {"METHODS": "POST"}, + "ROLE_PERMISSION": "ns_instances:id:" } }, - "ns_lcm_op_occs": {"METHODS": "GET", - "": {"METHODS": "GET"}, - }, - "vnfrs": {"METHODS": ("GET"), - "": {"METHODS": ("GET")} + "ns_instances": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "ns_instances:", + "": {"METHODS": ("GET", "DELETE"), + "ROLE_PERMISSION": "ns_instances:id:", + "scale": {"METHODS": ("POST",), + "ROLE_PERMISSION": "ns_instances:id:scale:" + }, + "terminate": {"METHODS": ("POST",), + "ROLE_PERMISSION": "ns_instances:id:terminate:" + }, + "instantiate": {"METHODS": ("POST",), + "ROLE_PERMISSION": "ns_instances:id:instantiate:" + }, + "action": {"METHODS": ("POST",), + "ROLE_PERMISSION": "ns_instances:id:action:" + }, + } + }, + "ns_lcm_op_occs": {"METHODS": ("GET",), + "ROLE_PERMISSION": "ns_instances:opps:", + "": {"METHODS": ("GET",), + "ROLE_PERMISSION": "ns_instances:opps:id:" + }, + }, + "vnfrs": {"METHODS": ("GET",), + "ROLE_PERMISSION": "vnf_instances:", + "": {"METHODS": ("GET",), + "ROLE_PERMISSION": "vnf_instances:id:" + } + }, + "vnf_instances": {"METHODS": ("GET",), + "ROLE_PERMISSION": "vnf_instances:", + "": {"METHODS": ("GET",), + "ROLE_PERMISSION": "vnf_instances:id:" + } }, - "vnf_instances": {"METHODS": ("GET"), - "": {"METHODS": ("GET")} - }, - } - }, - "nst": { - "v1": { - "netslice_templates_content": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "PUT", "DELETE")} - }, - "netslice_templates": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH", - "nst_content": {"METHODS": ("GET", "PUT")}, - "nst": {"METHODS": "GET"}, # descriptor inside package - "artifacts": {"*": {"METHODS": "GET"}} - } + "subscriptions": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "ns_subscriptions:", + "": {"METHODS": ("GET", "DELETE"), + "ROLE_PERMISSION": "ns_subscriptions:id:" + } + }, + } + }, + "nst": { + "v1": { + "netslice_templates_content": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "slice_templates:", + "": {"METHODS": ("GET", "PUT", "DELETE"), + "ROLE_PERMISSION": "slice_templates:id:", } }, - "subscriptions": {"TODO": ("GET", "POST"), - "": {"TODO": ("GET", "DELETE")} - }, - } - }, - "nsilcm": { - "v1": { - "netslice_instances_content": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE")} - }, - "netslice_instances": {"METHODS": ("GET", "POST"), + "netslice_templates": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "slice_templates:", + "": {"METHODS": ("GET", "DELETE"), + "TODO": ("PATCH",), + "ROLE_PERMISSION": "slice_templates:id:", + "nst_content": {"METHODS": ("GET", "PUT"), + "ROLE_PERMISSION": "slice_templates:id:content:" + }, + "nst": {"METHODS": ("GET",), # descriptor inside package + "ROLE_PERMISSION": "slice_templates:id:content:" + }, + "artifacts": {"METHODS": ("GET",), + "ROLE_PERMISSION": "slice_templates:id:content:", + "*": None + } + } + }, + "subscriptions": {"TODO": ("GET", "POST"), + "": {"TODO": ("GET", "DELETE")} + }, + } + }, + "nsilcm": { + "v1": { + "netslice_instances_content": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "slice_instances:", "": {"METHODS": ("GET", "DELETE"), - "terminate": {"METHODS": "POST"}, - "instantiate": {"METHODS": "POST"}, - "action": {"METHODS": "POST"}, + "ROLE_PERMISSION": "slice_instances:id:" } }, - "nsi_lcm_op_occs": {"METHODS": "GET", - "": {"METHODS": "GET"}, - }, - } - }, - "nspm": { - "v1": { - "pm_jobs": { - "": { - "reports": { - "": {"METHODS": ("GET")} - } - }, - }, + "netslice_instances": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "slice_instances:", + "": {"METHODS": ("GET", "DELETE"), + "ROLE_PERMISSION": "slice_instances:id:", + "terminate": {"METHODS": ("POST",), + "ROLE_PERMISSION": "slice_instances:id:terminate:" + }, + "instantiate": {"METHODS": ("POST",), + "ROLE_PERMISSION": "slice_instances:id:instantiate:" + }, + "action": {"METHODS": ("POST",), + "ROLE_PERMISSION": "slice_instances:id:action:" + }, + } + }, + "nsi_lcm_op_occs": {"METHODS": ("GET",), + "ROLE_PERMISSION": "slice_instances:opps:", + "": {"METHODS": ("GET",), + "ROLE_PERMISSION": "slice_instances:opps:id:", + }, + }, + } + }, + "nspm": { + "v1": { + "pm_jobs": { + "": { + "reports": { + "": {"METHODS": ("GET",), + "ROLE_PERMISSION": "reports:id:", + } + } }, }, - } + }, + }, +} + + +class NbiException(Exception): + + def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED): + Exception.__init__(self, message) + self.http_code = http_code + + +class Server(object): + instance = 0 + # to decode bytes to str + reader = getreader("utf-8") + + def __init__(self): + self.instance += 1 + self.authenticator = Authenticator(valid_url_methods, valid_query_string) + self.engine = Engine(self.authenticator) def _format_in(self, kwargs): try: @@ -368,7 +522,7 @@ class Server(object): cherrypy.request.headers.pop("Content-File-MD5", None) elif "application/yaml" in cherrypy.request.headers["Content-Type"]: error_text = "Invalid yaml format " - indata = yaml.load(cherrypy.request.body) + indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader) cherrypy.request.headers.pop("Content-File-MD5", None) elif "application/binary" in cherrypy.request.headers["Content-Type"] or \ "application/gzip" in cherrypy.request.headers["Content-Type"] or \ @@ -388,11 +542,11 @@ class Server(object): # "Only 'Content-Type' of type 'application/json' or # 'application/yaml' for input format are available") error_text = "Invalid yaml format " - indata = yaml.load(cherrypy.request.body) + indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader) cherrypy.request.headers.pop("Content-File-MD5", None) else: error_text = "Invalid yaml format " - indata = yaml.load(cherrypy.request.body) + indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader) cherrypy.request.headers.pop("Content-File-MD5", None) if not indata: indata = {} @@ -407,7 +561,7 @@ class Server(object): kwargs[k] = None elif format_yaml: try: - kwargs[k] = yaml.load(v) + kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader) except Exception: pass elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"): @@ -426,7 +580,7 @@ class Server(object): v[index] = None elif format_yaml: try: - v[index] = yaml.load(v[index]) + v[index] = yaml.load(v[index], Loader=yaml.SafeLoader) except Exception: pass @@ -439,18 +593,18 @@ class Server(object): raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST) @staticmethod - def _format_out(data, session=None, _format=None): + def _format_out(data, token_info=None, _format=None): """ return string of dictionary data according to requested json, yaml, xml. By default json :param data: response to be sent. Can be a dict, text or file - :param session: - :param _format: The format to be set as Content-Type ir data is a file + :param token_info: Contains among other username and project + :param _format: The format to be set as Content-Type if data is a file :return: None """ accept = cherrypy.request.headers.get("Accept") if data is None: if accept and "text/html" in accept: - return html.format(data, cherrypy.request, cherrypy.response, session) + return html.format(data, cherrypy.request, cherrypy.response, token_info) # cherrypy.response.status = HTTPStatus.NO_CONTENT.value return elif hasattr(data, "read"): # file object @@ -463,52 +617,47 @@ class Server(object): # TODO check that cherrypy close file. If not implement pending things to close per thread next return data if accept: - if "application/json" in accept: + if "text/html" in accept: + return html.format(data, cherrypy.request, cherrypy.response, token_info) + elif "application/yaml" in accept or "*/*" in accept: + pass + elif "application/json" in accept or (cherrypy.response.status and cherrypy.response.status >= 300): cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8' a = json.dumps(data, indent=4) + "\n" - return a.encode("utf8") - elif "text/html" in accept: - return html.format(data, cherrypy.request, cherrypy.response, session) - - elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept: - pass - # if there is not any valid accept, raise an error. But if response is already an error, format in yaml - elif cherrypy.response.status >= 400: - raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value, - "Only 'Accept' of type 'application/json' or 'application/yaml' " - "for output format are available") + return a.encode("utf8") cherrypy.response.headers["Content-Type"] = 'application/yaml' return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False, encoding='utf-8', allow_unicode=True) # , canonical=True, default_style='"' @cherrypy.expose def index(self, *args, **kwargs): - session = None + token_info = None try: if cherrypy.request.method == "GET": - session = self.authenticator.authorize() - outdata = "Index page" + token_info = self.authenticator.authorize() + outdata = token_info # Home page else: raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value, "Method {} not allowed for tokens".format(cherrypy.request.method)) - return self._format_out(outdata, session) + return self._format_out(outdata, token_info) except (EngineException, AuthException) as e: - cherrypy.log("index Exception {}".format(e)) + # cherrypy.log("index Exception {}".format(e)) cherrypy.response.status = e.http_code.value - return self._format_out("Welcome to OSM!", session) + return self._format_out("Welcome to OSM!", token_info) @cherrypy.expose def version(self, *args, **kwargs): # TODO consider to remove and provide version using the static version file - global __version__, version_date try: if cherrypy.request.method != "GET": raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED) elif args or kwargs: raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED) - return __version__ + " " + version_date + # TODO include version of other modules, pick up from some kafka admin message + osm_nbi_version = {"version": nbi_version, "date": nbi_version_date} + return self._format_out(osm_nbi_version) except NbiException as e: cherrypy.response.status = e.http_code.value problem_details = { @@ -518,59 +667,91 @@ class Server(object): } return self._format_out(problem_details, None) - @cherrypy.expose - def token(self, method, token_id=None, kwargs=None): - session = None - # self.engine.load_dbase(cherrypy.request.app.config) - indata = self._format_in(kwargs) - if not isinstance(indata, dict): - raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST) + def domain(self): try: - if method == "GET": - session = self.authenticator.authorize() - if token_id: - outdata = self.authenticator.get_token(session, token_id) - else: - outdata = self.authenticator.get_token_list(session) - elif method == "POST": - try: - session = self.authenticator.authorize() - except Exception: - session = None - if kwargs: - indata.update(kwargs) - outdata = self.authenticator.new_token(session, indata, cherrypy.request.remote) - session = outdata - cherrypy.session['Authorization'] = outdata["_id"] - self._set_location_header("admin", "v1", "tokens", outdata["_id"]) - # cherrypy.response.cookie["Authorization"] = outdata["id"] - # cherrypy.response.cookie["Authorization"]['expires'] = 3600 - elif method == "DELETE": - if not token_id and "id" in kwargs: - token_id = kwargs["id"] - elif not token_id: - session = self.authenticator.authorize() - token_id = session["_id"] - outdata = self.authenticator.del_token(token_id) - session = None - cherrypy.session['Authorization'] = "logout" - # cherrypy.response.cookie["Authorization"] = token_id - # cherrypy.response.cookie["Authorization"]['expires'] = 0 - else: - raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED) - return self._format_out(outdata, session) - except (NbiException, EngineException, DbException, AuthException) as e: - cherrypy.log("tokens Exception {}".format(e)) + domains = { + "user_domain_name": cherrypy.tree.apps['/osm'].config["authentication"].get("user_domain_name"), + "project_domain_name": cherrypy.tree.apps['/osm'].config["authentication"].get("project_domain_name")} + return self._format_out(domains) + except NbiException as e: cherrypy.response.status = e.http_code.value problem_details = { "code": e.http_code.name, "status": e.http_code.value, "detail": str(e), } - return self._format_out(problem_details, session) + return self._format_out(problem_details, None) + + @staticmethod + def _format_login(token_info): + """ + Changes cherrypy.request.login to include username/project_name;session so that cherrypy access log will + log this information + :param token_info: Dictionary with token content + :return: None + """ + cherrypy.request.login = token_info.get("username", "-") + if token_info.get("project_name"): + cherrypy.request.login += "/" + token_info["project_name"] + if token_info.get("id"): + cherrypy.request.login += ";session=" + token_info["id"][0:12] + + @cherrypy.expose + def token(self, method, token_id=None, kwargs=None): + token_info = None + # self.engine.load_dbase(cherrypy.request.app.config) + indata = self._format_in(kwargs) + if not isinstance(indata, dict): + raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST) + + if method == "GET": + token_info = self.authenticator.authorize() + # for logging + self._format_login(token_info) + if token_id: + outdata = self.authenticator.get_token(token_info, token_id) + else: + outdata = self.authenticator.get_token_list(token_info) + elif method == "POST": + try: + token_info = self.authenticator.authorize() + except Exception: + token_info = None + if kwargs: + indata.update(kwargs) + # This is needed to log the user when authentication fails + cherrypy.request.login = "{}".format(indata.get("username", "-")) + outdata = token_info = self.authenticator.new_token(token_info, indata, cherrypy.request.remote) + cherrypy.session['Authorization'] = outdata["_id"] + self._set_location_header("admin", "v1", "tokens", outdata["_id"]) + # for logging + self._format_login(token_info) + + # cherrypy.response.cookie["Authorization"] = outdata["id"] + # cherrypy.response.cookie["Authorization"]['expires'] = 3600 + elif method == "DELETE": + if not token_id and "id" in kwargs: + token_id = kwargs["id"] + elif not token_id: + token_info = self.authenticator.authorize() + # for logging + self._format_login(token_info) + token_id = token_info["_id"] + outdata = self.authenticator.del_token(token_id) + token_info = None + cherrypy.session['Authorization'] = "logout" + # cherrypy.response.cookie["Authorization"] = token_id + # cherrypy.response.cookie["Authorization"]['expires'] = 0 + else: + raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED) + return self._format_out(outdata, token_info) @cherrypy.expose def test(self, *args, **kwargs): + if not cherrypy.config.get("server.enable_test") or (isinstance(cherrypy.config["server.enable_test"], str) and + cherrypy.config["server.enable_test"].lower() == "false"): + cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value + return "test URL is disabled" thread_info = None if args and args[0] == "help": return "
\ninit\nfile/  download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
@@ -628,14 +809,15 @@ class Server(object):
             return_text = "
{} ->\n".format(main_topic)
             try:
                 if cherrypy.request.method == 'POST':
-                    to_send = yaml.load(cherrypy.request.body)
+                    to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
                     for k, v in to_send.items():
                         self.engine.msg.write(main_topic, k, v)
                         return_text += "  {}: {}\n".format(k, v)
                 elif cherrypy.request.method == 'GET':
                     for k, v in kwargs.items():
-                        self.engine.msg.write(main_topic, k, yaml.load(v))
-                        return_text += "  {}: {}\n".format(k, yaml.load(v))
+                        v_dict = yaml.load(v, Loader=yaml.SafeLoader)
+                        self.engine.msg.write(main_topic, k, v_dict)
+                        return_text += "  {}: {}\n".format(k, v_dict)
             except Exception as e:
                 return_text += "Error: " + str(e)
             return_text += "
\n" @@ -661,11 +843,12 @@ class Server(object): return_text += "
" return return_text - def _check_valid_url_method(self, method, *args): + @staticmethod + def _check_valid_url_method(method, *args): if len(args) < 3: raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED) - reference = self.valid_methods + reference = valid_url_methods for arg in args: if arg is None: break @@ -678,7 +861,9 @@ class Server(object): elif "" in reference: reference = reference[""] elif "*" in reference: - reference = reference["*"] + # if there is content + if reference["*"]: + reference = reference["*"] break else: raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED) @@ -686,7 +871,7 @@ class Server(object): raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED) elif "METHODS" in reference and method not in reference["METHODS"]: raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED) - return + return reference["ROLE_PERMISSION"] + method.lower() @staticmethod def _set_location_header(main_topic, version, topic, id): @@ -703,11 +888,25 @@ class Server(object): return @staticmethod - def _manage_admin_query(session, kwargs, method, _id): + def _extract_query_string_operations(kwargs, method): + """ + + :param kwargs: + :return: + """ + query_string_operations = [] + if kwargs: + for qs in ("FORCE", "PUBLIC", "ADMIN", "SET_PROJECT"): + if qs in kwargs and kwargs[qs].lower() != "false": + query_string_operations.append(qs.lower() + ":" + method.lower()) + return query_string_operations + + @staticmethod + def _manage_admin_query(token_info, kwargs, method, _id): """ Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT Check that users has rights to use them and returs the admin_query - :param session: session rights obtained by token + :param token_info: token_info rights obtained by token :param kwargs: query string input. :param method: http method: GET, POSST, PUT, ... :param _id: @@ -718,8 +917,9 @@ class Server(object): set_project: tuple with projects that a created element will belong to method: show, list, delete, write """ - admin_query = {"force": False, "project_id": (session["project_id"], ), "username": session["username"], - "admin": session["admin"], "public": None} + admin_query = {"force": False, "project_id": (token_info["project_id"], ), "username": token_info["username"], + "admin": token_info["admin"], "public": None, + "allow_show_user_project_role": token_info["allow_show_user_project_role"]} if kwargs: # FORCE if "FORCE" in kwargs: @@ -737,7 +937,7 @@ class Server(object): if "ADMIN" in kwargs: behave_as = kwargs.pop("ADMIN") if behave_as.lower() != "false": - if not session["admin"]: + if not token_info["admin"]: raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus.UNAUTHORIZED) if not behave_as or behave_as.lower() == "true": # convert True, None to empty list admin_query["project_id"] = () @@ -762,7 +962,7 @@ class Server(object): # PROJECT_READ # if "PROJECT_READ" in kwargs: # admin_query["project"] = kwargs.pop("project") - # if admin_query["project"] == session["project_id"]: + # if admin_query["project"] == token_info["project_id"]: if method == "GET": if _id: admin_query["method"] = "show" @@ -776,13 +976,13 @@ class Server(object): @cherrypy.expose def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs): - session = None + token_info = None outdata = None _format = None method = "DONE" engine_topic = None rollback = [] - session = None + engine_session = None try: if not main_topic or not version or not topic: raise NbiException("URL must contain at least 'main_topic/version/topic'", @@ -798,18 +998,17 @@ class Server(object): else: method = cherrypy.request.method - self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args) - + role_permission = self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args) + query_string_operations = self._extract_query_string_operations(kwargs, method) if main_topic == "admin" and topic == "tokens": return self.token(method, _id, kwargs) - - # self.engine.load_dbase(cherrypy.request.app.config) - session = self.authenticator.authorize() - session = self._manage_admin_query(session, kwargs, method, _id) + token_info = self.authenticator.authorize(role_permission, query_string_operations, _id) + if main_topic == "admin" and topic == "domains": + return self.domain() + engine_session = self._manage_admin_query(token_info, kwargs, method, _id) indata = self._format_in(kwargs) engine_topic = topic - if topic == "subscriptions": - engine_topic = main_topic + "_" + topic + if item and topic != "pm_jobs": engine_topic = item @@ -817,6 +1016,10 @@ class Server(object): engine_topic = "nsds" elif main_topic == "vnfpkgm": engine_topic = "vnfds" + if topic == "vnfpkg_op_occs": + engine_topic = "vnfpkgops" + if topic == "vnf_packages" and item == "action": + engine_topic = "vnfpkgops" elif main_topic == "nslcm": engine_topic = "nsrs" if topic == "ns_lcm_op_occs": @@ -834,6 +1037,9 @@ class Server(object): if engine_topic == "vims": # TODO this is for backward compatibility, it will be removed in the future engine_topic = "vim_accounts" + if topic == "subscriptions": + engine_topic = main_topic + "_" + topic + if method == "GET": if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"): if item in ("vnfd", "nsd", "nst"): @@ -844,22 +1050,25 @@ class Server(object): path = () else: path = None - file, _format = self.engine.get_file(session, engine_topic, _id, path, + file, _format = self.engine.get_file(engine_session, engine_topic, _id, path, cherrypy.request.headers.get("Accept")) outdata = file elif not _id: - outdata = self.engine.get_item_list(session, engine_topic, kwargs) + outdata = self.engine.get_item_list(engine_session, engine_topic, kwargs, api_req=True) else: if item == "reports": # TODO check that project_id (_id in this context) has permissions _id = args[0] - outdata = self.engine.get_item(session, engine_topic, _id) + outdata = self.engine.get_item(engine_session, engine_topic, _id, True) + elif method == "POST": + cherrypy.response.status = HTTPStatus.CREATED.value if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"): _id = cherrypy.request.headers.get("Transaction-Id") if not _id: - _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers) - completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs, + _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, {}, None, + cherrypy.request.headers) + completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs, cherrypy.request.headers) if completed: self._set_location_header(main_topic, version, topic, _id) @@ -868,101 +1077,126 @@ class Server(object): outdata = {"id": _id} elif topic == "ns_instances_content": # creates NSR - _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs) + _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs) # creates nslcmop indata["lcmOperationType"] = "instantiate" indata["nsInstanceId"] = _id - nslcmop_id = self.engine.new_item(rollback, session, "nslcmops", indata, None) + nslcmop_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, None) self._set_location_header(main_topic, version, topic, _id) outdata = {"id": _id, "nslcmop_id": nslcmop_id} elif topic == "ns_instances" and item: indata["lcmOperationType"] = item indata["nsInstanceId"] = _id - _id = self.engine.new_item(rollback, session, "nslcmops", indata, kwargs) + _id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, kwargs) self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id) outdata = {"id": _id} cherrypy.response.status = HTTPStatus.ACCEPTED.value elif topic == "netslice_instances_content": # creates NetSlice_Instance_record (NSIR) - _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs) + _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs) self._set_location_header(main_topic, version, topic, _id) indata["lcmOperationType"] = "instantiate" indata["netsliceInstanceId"] = _id - nsilcmop_id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs) + nsilcmop_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs) outdata = {"id": _id, "nsilcmop_id": nsilcmop_id} - elif topic == "netslice_instances" and item: indata["lcmOperationType"] = item indata["netsliceInstanceId"] = _id - _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs) + _id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs) self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id) outdata = {"id": _id} cherrypy.response.status = HTTPStatus.ACCEPTED.value + elif topic == "vnf_packages" and item == "action": + indata["lcmOperationType"] = item + indata["vnfPkgId"] = _id + _id, _ = self.engine.new_item(rollback, engine_session, "vnfpkgops", indata, kwargs) + self._set_location_header(main_topic, version, "vnfpkg_op_occs", _id) + outdata = {"id": _id} + cherrypy.response.status = HTTPStatus.ACCEPTED.value + elif topic == "subscriptions": + _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs) + self._set_location_header(main_topic, version, topic, _id) + link = {} + link["self"] = cherrypy.response.headers["Location"] + outdata = {"id": _id, "filter": indata["filter"], "callbackUri": indata["CallbackUri"], + "_links": link} + cherrypy.response.status = HTTPStatus.CREATED.value else: - _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, - cherrypy.request.headers) + _id, op_id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs, + cherrypy.request.headers) self._set_location_header(main_topic, version, topic, _id) outdata = {"id": _id} + if op_id: + outdata["op_id"] = op_id + cherrypy.response.status = HTTPStatus.ACCEPTED.value # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages") - cherrypy.response.status = HTTPStatus.CREATED.value elif method == "DELETE": if not _id: - outdata = self.engine.del_item_list(session, engine_topic, kwargs) + outdata = self.engine.del_item_list(engine_session, engine_topic, kwargs) cherrypy.response.status = HTTPStatus.OK.value else: # len(args) > 1 - delete_in_process = False - if topic == "ns_instances_content" and not session["force"]: + # for NS NSI generate an operation + op_id = None + if topic == "ns_instances_content" and not engine_session["force"]: nslcmop_desc = { "lcmOperationType": "terminate", "nsInstanceId": _id, "autoremove": True } - opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None) - if opp_id: - delete_in_process = True - outdata = {"_id": opp_id} - cherrypy.response.status = HTTPStatus.ACCEPTED.value - elif topic == "netslice_instances_content" and not session["force"]: + op_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", nslcmop_desc, kwargs) + if op_id: + outdata = {"_id": op_id} + elif topic == "netslice_instances_content" and not engine_session["force"]: nsilcmop_desc = { "lcmOperationType": "terminate", "netsliceInstanceId": _id, "autoremove": True } - opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None) - if opp_id: - delete_in_process = True - outdata = {"_id": opp_id} - cherrypy.response.status = HTTPStatus.ACCEPTED.value - if not delete_in_process: - self.engine.del_item(session, engine_topic, _id) - cherrypy.response.status = HTTPStatus.NO_CONTENT.value - if engine_topic in ("vim_accounts", "wim_accounts", "sdns"): - cherrypy.response.status = HTTPStatus.ACCEPTED.value + op_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", nsilcmop_desc, None) + if op_id: + outdata = {"_id": op_id} + # if there is not any deletion in process, delete + if not op_id: + op_id = self.engine.del_item(engine_session, engine_topic, _id) + if op_id: + outdata = {"op_id": op_id} + cherrypy.response.status = HTTPStatus.ACCEPTED.value if op_id else HTTPStatus.NO_CONTENT.value elif method in ("PUT", "PATCH"): - outdata = None - if not indata and not kwargs and not session.get("set_project"): + op_id = None + if not indata and not kwargs and not engine_session.get("set_project"): raise NbiException("Nothing to update. Provide payload and/or query string", HTTPStatus.BAD_REQUEST) if item in ("nsd_content", "package_content", "nst_content") and method == "PUT": - completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs, + completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs, cherrypy.request.headers) if not completed: cherrypy.response.headers["Transaction-Id"] = id else: - self.engine.edit_item(session, engine_topic, _id, indata, kwargs) - cherrypy.response.status = HTTPStatus.NO_CONTENT.value + op_id = self.engine.edit_item(engine_session, engine_topic, _id, indata, kwargs) + + if op_id: + cherrypy.response.status = HTTPStatus.ACCEPTED.value + outdata = {"op_id": op_id} + else: + cherrypy.response.status = HTTPStatus.NO_CONTENT.value + outdata = None else: raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED) # if Role information changes, it is needed to reload the information of roles if topic == "roles" and method != "GET": self.authenticator.load_operation_to_allowed_roles() - return self._format_out(outdata, session, _format) + + if topic == "projects" and method == "DELETE" \ + or topic in ["users", "roles"] and method in ["PUT", "PATCH", "DELETE"]: + self.authenticator.remove_token_from_cache() + + return self._format_out(outdata, token_info, _format) except Exception as e: if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException, - ValidationError)): + ValidationError, AuthconnException)): http_code_value = cherrypy.response.status = e.http_code.value http_code_name = e.http_code.name cherrypy.log("Exception {}".format(e)) @@ -979,6 +1213,9 @@ class Server(object): if rollback_item.get("operation") == "set": self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]}, rollback_item["content"], fail_on_empty=False) + elif rollback_item.get("operation") == "del_list": + self.engine.db.del_list(rollback_item["topic"], rollback_item["filter"], + fail_on_empty=False) else: self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]}, fail_on_empty=False) @@ -994,8 +1231,15 @@ class Server(object): "status": http_code_value, "detail": error_text, } - return self._format_out(problem_details, session) + return self._format_out(problem_details, token_info) # raise cherrypy.HTTPError(e.http_code.value, str(e)) + finally: + if token_info: + self._format_login(token_info) + if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict): + for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"): + if outdata.get(logging_id): + cherrypy.request.login += ";{}={}".format(logging_id, outdata[logging_id][:36]) def _start_service(): @@ -1096,13 +1340,9 @@ def _start_service(): subscription_thread.start() # Do not capture except SubscriptionException - # load and print version. Ignore possible errors, e.g. file not found - try: - with open("{}/version".format(engine_config["/static"]['tools.staticdir.dir'])) as version_file: - version_data = version_file.read() - cherrypy.log.error("Starting OSM NBI Version: {}".format(version_data.replace("\n", " "))) - except Exception: - pass + backend = engine_config["authentication"]["backend"] + cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend" + .format(nbi_version, nbi_version_date, backend)) def _stop_service():