From b83af618c53fe23fa547ef64103d8bc38ce5cf04 Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Wed, 8 Oct 2025 12:00:49 -0500 Subject: [PATCH] added security & streamlined processing @TODO: add restrictions --- bin/qcms | 3 +- cms/__init__.py | 162 +++++-------- cms/disk.py | 81 ++++--- cms/engine/__init__.py | 374 ----------------------------- cms/engine/plugins/__init__.py | 45 +--- cms/index.py | 256 +++++++++++++------- cms/meta/__init__.py | 2 +- cms/secure.py | 174 ++++++++++++++ cms/sites/__init__.py | 374 +++++++++++++++++++++++++---- cms/static/css/qcms-login.css | 28 +++ cms/static/js/qcms/menu.js | 14 +- cms/static/js/qcms/page-loader.js | 16 +- cms/templates/404.html | 54 ++--- cms/templates/500.html | 69 ++++++ cms/templates/index.html | 59 +---- cms/templates/libs.html | 43 ++++ cms/templates/login/login.html | 32 +++ cms/templates/login/nextcloud.html | 24 ++ cms/templates/login/oauth2.html | 71 ++++++ cms/templates/login/pam.html | 19 ++ cms/templates/user.html | 7 + pyproject.toml | 8 +- 22 files changed, 1133 insertions(+), 782 deletions(-) create mode 100644 cms/secure.py create mode 100644 cms/static/css/qcms-login.css create mode 100644 cms/templates/500.html create mode 100644 cms/templates/libs.html create mode 100644 cms/templates/login/login.html create mode 100644 cms/templates/login/nextcloud.html create mode 100644 cms/templates/login/oauth2.html create mode 100644 cms/templates/login/pam.html create mode 100644 cms/templates/user.html diff --git a/bin/qcms b/bin/qcms index 065541a..0ab1ff6 100755 --- a/bin/qcms +++ b/bin/qcms @@ -175,7 +175,8 @@ def secure( """ print (_msg) - +# def add_login(manifest:Annotated[str,typer.Argument(help="path of the manifest file")],) : +# pass def load(**_args): """ This function will load external module form a given location and return a pointer to a function in a given module diff --git a/cms/__init__.py b/cms/__init__.py index d6c8aa1..18e5bab 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -9,7 +9,7 @@ import json from . import sites from . import apexchart from . import meta - +from . import secure class Plugin : # # decorator for plugin functions, this is a preliminary to enable future developement @@ -29,46 +29,7 @@ class Plugin : setattr(wrapper,'method',self._method) setattr(wrapper,'mimetype',self._mimetype) return wrapper - # @staticmethod - # def load(_path,_filename,_name) : - # """ - # This function will load external module form a given location and return a pointer to a function in a given module - # :path absolute path of the file (considered plugin) to be loaded - # :name name of the function to be applied - # """ - # # _path = _args['path'] #os.sep.join([_args['root'],'plugin']) - - # if os.path.isdir(_path): - # files = os.listdir(_path) - # if files : - # files = [name for name in files if name.endswith('.py') and name == _filename] - # if files: - # _path = os.sep.join([_path,files[0]]) - # else: - # return None - # else: - # return None - # #-- We have a file ... - # # _name = _args['name'] - # spec = importlib.util.spec_from_file_location(_filename, _path) - # module = importlib.util.module_from_spec(spec) - # spec.loader.exec_module(module) - # # - # # we need to make sure we have the plugin decorator here to make sure - - # return getattr(module,_name) if hasattr(module,_name) else None - - # @staticmethod - # def stats (_config) : - # """ - # Returns the statistics of the plugins - # """ - # _data = [] - # for _name in _config : - # _log = {"files":_name,"loaded":len(_config[_name]),"logs":json.dumps(_config[_name])} - # _data.append(_log) - # return pd.DataFrame(_data) - + @staticmethod def call(**_args): """ @@ -124,66 +85,63 @@ class Plugin : return _data,_code,{'Content-Type':_mimeType} pass -class delegate: - def __call__(self,**_args) : - _handler= _args['handler'] #-- module handler - _request= _args['request'] - _uri = _args['uri'] #_request.path[1:] - # - # we need to update the _uri (if context/embeded apps) - # - # _context = _handler.system()['context'] - _context = _handler.get('system.context') - if _context : - _uri = f'{_context}/{_uri}' - # print ([' ** ',_uri, _args['uri']]) - # _plugins = _handler.plugins() - _plugins = _handler.get('plugins') - _code = 200 - - if _uri in _plugins : - # _config = _handler.config() #_args['config'] - _config = _handler.get(None) #_args['config'] + + +# +# default plugins to load into the configuration file +@Plugin(mimetype="application/json") +def authorizationURL (**_args): + # _config = _args['config'] + _source = _args['config']['system']['source'] + # + # + if 'secure' in _source : + _path = _source['secure']['path'] + f = open(_path) + _config = json.loads(f.read()) + if _config['method'] in ['oauth2','oauth20','oauth2.0'] : + _url = [f"{_key}={_config[_key]}" for _key in _config if _key not in ['method','authorization_url']] + + # return _url + _url = _config['authorization_url']+'?'+'&'.join(_url) + return {"url":_url,"label":f"Login with {_source['secure']['provider']}"} + else: + return {} - _pointer = _plugins[_uri] - _data = {} - if hasattr(_pointer,'mimetype') and hasattr(_pointer,'method'): - # - # we constraint the methods given their execution ... - _mimeType = _pointer.mimetype - if _request.method in _pointer.method : - _data = _pointer(request=_request,config=_config) - elif not hasattr(_pointer,'mimetype'): - - _data,_mimeType = _pointer(request=_request,config=_config) - - else: - _mimeType = 'application/octet-stream' - # _data = _pointer(request=_request,config=_config) - if type(_data) == pd.DataFrame : - _data = _data.to_json(orient='records') - elif type(_data) in [dict,list] : - - _data = json.dumps(_data) - pass - else: - _code = 404 - # - # We should generate a 500 error in this case with a message ... - # - _mimeType = 'plain/html' - _data = f""" - - """ - # if _mimeType in ['text/html','plain/html'] : - # _env = Environment(loader=BaseLoader()).from_string(_data) - # _kwargs = {'layout':_config['layout'],'system':_config['system']} - # print (_data) - # _data = _env.render(**_kwargs) - return _data,_code,{'Content-Type':_mimeType} +@Plugin(mimetype="text/html") +def oauthFinalize (**_args): + _request = _args['request'] + + _html = """ + + + + + +
+ + +
Please wait ...
+
+ """ + return _html \ No newline at end of file diff --git a/cms/disk.py b/cms/disk.py index 18cbbb3..04228b5 100644 --- a/cms/disk.py +++ b/cms/disk.py @@ -80,39 +80,40 @@ def _realpath (uri,_config) : return _uri -def _format (uri,_config): - _layout = _config['layout'] - if 'location' in _layout : - return 'api/disk/read?uri='+uri - return uri -def read (**_args): - """ - This will read binary files from disk, and allow the location or not to be read - @TODO: add permissions otherwise there can be disk-wide reads - """ - request = _args['request'] if 'request' in _args else None - _layout = _args['config']['layout'] - _uri = request.args['uri'] if request else _args['uri'] # if 'location' in _layout : - # _uri = os.sep.join([_layout['location'],_uri]) - _uri = _realpath(_uri, _args['config']) - _mimeType = 'text/plain' +# def _format (uri,_config): +# _layout = _config['layout'] +# if 'location' in _layout : +# return 'api/disk/read?uri='+uri +# return uri +# def read (**_args): +# """ +# This will read binary files from disk, and allow the location or not to be read +# @TODO: add permissions otherwise there can be disk-wide reads +# """ +# request = _args['request'] if 'request' in _args else None +# _layout = _args['config']['layout'] +# _uri = request.args['uri'] if request else _args['uri'] # if 'location' in _layout : +# # _uri = os.sep.join([_layout['location'],_uri]) +# _uri = _realpath(_uri, _args['config']) +# _mimeType = 'text/plain' +# _stream = None +# if os.path.exists(_uri): +# f = open(_uri,mode='rb') +# _stream = f.read() +# f.close() +# # +# # Inferring the type of the data to be returned +# _mimeType = 'application/octet-stream' +# _extension = _uri.split('.')[-1] +# if _extension in ['css','js','csv','html'] : +# _mimeType = f'text/{_extension}' +# if _extension == 'js' : +# _mimeType = 'text/javascript' +# elif _extension in ['png','jpg','jpeg'] : +# _mimeType = f'image/{_extension}' - if os.path.exists(_uri): - f = open(_uri,mode='rb') - _stream = f.read() - f.close() - # - # Inferring the type of the data to be returned - _mimeType = 'application/octet-stream' - _extension = _uri.split('.')[-1] - if _extension in ['css','js','csv','html'] : - _mimeType = f'text/{_extension}' - if _extension == 'js' : - _mimeType = 'text/javascript' - elif _extension in ['png','jpg','jpeg'] : - _mimeType = f'image/{_extension}' - return _stream, _mimeType - return None,_mimeType +# return _stream, _mimeType + # return None,_mimeType def exists(**_args): _path = _realpath(_args['uri'],_args['config']) @@ -133,12 +134,15 @@ def html(_uri,_config) : _layout = _config['layout'] if 'location' in _layout : if not _config : - _api = os.sep.join(['api/disk/read?uri=',copy.copy(_layout['root']) ]) + # _api = os.sep.join(['api/disk/read?uri=',copy.copy(_layout['root']) ]) + _api = '/'.join(['files',copy.copy(_layout['root'])]) else: - _api = os.sep.join([f'{_context}/api/disk/read?uri=',copy.copy(_layout['root'])]) + # _api = os.sep.join([f'{_context}/api/disk/read?uri=',copy.copy(_layout['root'])]) + _api = '/'.join([f'{_context}/files',copy.copy(_layout['root'])]) if f"{_layout['root']}" in _html : + # _html = _html.replace('/api/disk/read?uri=','').replace(f"{_layout['root']}",_api) _html = _html.replace('/api/disk/read?uri=','').replace(f"{_layout['root']}",_api) _html = mistune.html(_html).replace(""",'"').replace("<","<").replace(">",">") if _uri[-2:] in ['md','MD','Md','mD'] else _html # _html = _html.replace(f'{os.sep}{_layout["root"]}',_layout['root']) @@ -154,10 +158,11 @@ def plugins (**_args): """ _context = _args['context'] if 'path' not in _args : - key = 'api/disk/read' - if _context : - key = f'{_context}/{key}' - return {key:read} + # key = 'api/disk/read' + # if _context : + # key = f'{_context}/{key}' + # return {key:read} + return None _path = _args['path'] #os.sep.join([_args['root'],'plugin']) loader = plugin_ix.Loader(file=_path) diff --git a/cms/engine/__init__.py b/cms/engine/__init__.py index ed826f4..1a1129d 100644 --- a/cms/engine/__init__.py +++ b/cms/engine/__init__.py @@ -9,377 +9,3 @@ # import importlib # import importlib.util from cms import disk, cloud -# from . import basic - -# class Loader : -# """ -# This class is designed to exclusively load configuration from disk into an object -# :path path to the configuraiton file -# :location original location (caller) -# """ -# def __init__(self,**_args): -# self._path = _args['path'] -# self._original_location = None if 'location' not in _args else _args['location'] -# self._location = None -# self._caller = None if 'caller' not in _args else _args['caller'] -# print ([' *** ', self._caller]) -# self._menu = {} -# self._plugins={} -# self.load() - - -# def load(self): -# """ -# This function will load menu (overwrite) and plugins -# """ -# self.init_config() -# self.init_menu() -# self.init_plugins() -# def init_config(self): -# """ -# Initialize & loading configuration from disk -# """ -# f = open (self._path) -# self._config = json.loads(f.read()) - -# if self._caller : -# self._location = self._original_location.split(os.sep) # needed for plugin loading -# self._location = os.sep.join(self._location[:-1]) -# self._config['system']['portal'] = self._caller != None - -# # -# # let's see if we have a location for a key (i.e security key) in the configuration -# # -# self.update_config() -# # _system = self._config['system'] -# # if 'source' in _system and 'key' in _system['source'] : -# # _path = _system['source']['key'] -# # if os.path.exists(_path): -# # f = open(_path) -# # _system['source']['key'] = f.read() -# # f.close() -# # self._system = _system -# # self._config['system'] = _system -# def update_config(self): -# """ -# We are going to update the configuration (source.key, layout.root) -# """ -# _system = self._config['system'] -# # -# # updating security-key that allows the application to update on-demand -# if 'source' in _system and 'key' in _system['source'] : -# _path = _system['source']['key'] -# if os.path.exists(_path): -# f = open(_path) -# _system['source']['key'] = f.read() -# f.close() -# self._system = _system -# self._config['system'] = _system -# _layout = self._config['layout'] -# # -# # update root so that the app can be launched from anywhere -# # This would help reduce the footprint of the app/framework -# _path = os.sep.join(self._path.split(os.sep)[:-1]) -# _p = 'source' not in _system -# _q = 'source' in _system and _system['source']['id'] != 'cloud' -# _r = os.path.exists(_layout['root']) -# if not _r and (_p or _q) : -# # -# # If we did running this app from installed framework (this should not apply to dependent apps) -# # -# _root = os.sep.join([_path,_layout['root']]) -# self._config['layout']['root'] = _root -# self._config['layout']['root_prefix'] = _root - -# def init_menu(self): -# """ -# This function will read menu and sub-menu items from disk structure, -# The files are loaded will -# """ - -# _config = self._config -# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' : -# _sourceHandler = cloud -# else: -# _sourceHandler = disk -# _object = _sourceHandler.build(_config) - -# # -# # After building the site's menu, let us add the one from 3rd party apps -# # - - -# _layout = copy.deepcopy(_config['layout']) -# _overwrite = _layout['overwrite'] if 'overwrite' in _layout else {} - -# # -# # @TODO: Find a way to translate rename/replace keys of the _object (menu) here -# # -# #-- applying overwrites to the menu items -# for _name in _object : -# _submenu = _object[_name] -# _index = 0 -# for _item in _submenu : -# text = _item['text'].strip() -# if text in _overwrite : -# if 'uri' in _item and 'url' in 'url' in _overwrite[text] : -# del _item['uri'] -# _item = dict(_item,**_overwrite[text]) - -# if 'uri' in _item and 'type' in _item and _item['type'] != 'open': -# _item['uri'] = _item['uri'].replace(_layout['root'],'') - -# _submenu[_index] = _item -# _index += 1 -# self.init_apps(_object) -# self._menu = _object -# self._order() - -# def init_apps (self,_menu): -# """ -# Insuring that the apps are loaded into the menu with an approriate label -# """ -# _system = self._config['system'] -# _context = _system['context'] -# if 'routes' in _system : -# # _items = [] -# _overwrite = {} if 'overwrite' not in self._config['layout'] else self._config['layout']['overwrite'] -# for _text in _system['routes'] : -# _item = _system['routes'][_text] -# if 'menu' not in _item : -# continue -# uri = f'{_context}/{_text}' -# # _items.append ({"text":_text,'uri':uri,'type':'open'}) -# _label = _item['menu'] -# if _label not in _menu : -# _menu [_label] = [] -# _menu[_label].append ({"text":_text,'uri':uri,'type':'open'}) -# # _overwrite[_text] = {'text': _text.replace('-',' ').replace('_',' '),'uri':uri,'type':'open'} -# # _menu['products'] = _items -# # -# # given that the menu items assumes redirecting to a page ... -# # This is not the case -# # -# # self._config['overwrite'] = _overwrite -# else: -# pass - -# pass -# def _order (self): -# _config = self._config -# if 'order' in _config['layout'] and 'menu' in _config['layout']['order']: -# _sortedmenu = {} -# _menu = self._menu -# for _name in _config['layout']['order']['menu'] : -# if _name in _menu : -# _sortedmenu[_name] = _menu[_name] - -# _menu = _sortedmenu if _sortedmenu else _menu -# # -# # If there are missing items in the sorting -# _missing = list(set(self._menu.keys()) - set(_sortedmenu)) -# if _missing : -# for _name in _missing : -# _menu[_name] = self._menu[_name] -# _config['layout']['menu'] = _menu #cms.components.menu(_config) -# self._menu = _menu -# self._config = _config -# def init_plugins(self) : -# """ -# This function looks for plugins in the folder on disk (no cloud support) and attempts to load them -# """ -# _config = self._config -# PATH= os.sep.join([_config['layout']['root'],'_plugins']) -# if not os.path.exists(PATH) : -# # -# # we need to determin if there's an existing -# PATH = os.sep.join(self._path.split(os.sep)[:-1]+ [PATH] ) -# if not os.path.exists(PATH) and self._location and os.path.exists(self._location) : -# # -# # overriding the location of plugins ... -# PATH = os.sep.join([self._location, _config['layout']['root'],'_plugins']) - -# _map = {} -# # if not os.path.exists(PATH) : -# # return _map -# if 'plugins' not in _config : -# _config['plugins'] = {} -# _conf = _config['plugins'] - -# for _key in _conf : - -# _path = os.sep.join([PATH,_key+".py"]) -# if not os.path.exists(_path): -# continue -# for _name in _conf[_key] : -# _pointer = self._load_plugin(path=_path,name=_name) -# if _pointer : -# _uri = "/".join(["api",_key,_name]) -# _map[_uri] = _pointer -# # -# # We are adding some source specific plugins to the user-defined plugins -# # This is intended to have out-of the box plugins... -# # -# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' : -# _plugins = cloud.plugins() -# else: -# _plugins = disk.plugins() -# # -# # If there are any plugins found, we should load them and use them - -# if _plugins : -# _map = dict(_map,**_plugins) -# else: -# pass -# self._plugins = _map -# self._config['plugins'] = self._plugins - -# def _load_plugin(self,**_args): -# """ -# This function will load external module form a given location and return a pointer to a function in a given module -# :path absolute path of the file (considered plugin) to be loaded -# :name name of the function to be applied -# """ -# _path = _args['path'] #os.sep.join([_args['root'],'plugin']) -# if os.path.isdir(_path): -# files = os.listdir(_path) -# if files : -# files = [name for name in files if name.endswith('.py')] -# if files: -# _path = os.sep.join([_path,files[0]]) -# else: -# return None -# else: -# return None -# #-- We have a file ... -# _name = _args['name'] - -# spec = importlib.util.spec_from_file_location(_name, _path) -# module = importlib.util.module_from_spec(spec) -# spec.loader.exec_module(module) - -# return getattr(module,_name) if hasattr(module,_name) else None - -# class Getter (Loader): -# def __init__(self,**_args): -# super().__init__(**_args) -# def load(self): -# super().load() -# _system = self.system() -# _logo = _system['logo'] -# if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'): - -# _icon = f'/api/cloud/download?doc=/{_logo}' -# _system['icon'] = _icon - -# else: -# _root = self._config['layout']['root'] -# _icon = os.sep.join([_root,_logo]) -# _system['icon'] = _logo - -# self._config['system'] = _system -# if self._caller : -# _system['caller'] = {'icon': self._caller.system()['icon']} -# def html(self,uri,id,_args={},_system={}) : -# """ -# This function reads a given uri and returns the appropriate html document, and applies environment context - -# """ -# _system = self._config['system'] -# if 'source' in _system and _system['source']['id'] == 'cloud': -# _html = cloud.html(uri,dict(_args,**{'system':_system})) - -# else: - -# _html = disk.html(uri,self.layout()) -# # _html = (open(uri)).read() - - -# #return ' '.join(['
'.replace(':id',id),_html,'
']) -# _html = ' '.join(['
'.replace(':id',id),_html,'
']) -# appContext = Environment(loader=BaseLoader()).from_string(_html) -# _args['system'] = _system -# # -# # If the rendering of the HTML happens here we should plugin custom functions (at the very least) -# # - -# return appContext.render(**_args) -# # return _html - -# def data (self,_args): -# """ -# :store data-store parameters (data-transport, github.com/lnyemba/data-transport) -# :query query to be applied against the store (expected data-frame) -# """ -# _store = _args['store'] -# reader = transport.factory.instance(**_store) -# _queries= copy.deepcopy(_store['query']) -# _data = reader.read(**_queries) -# return _data -# def csv(self,uri) : -# return pd.read(uri).to_html() - -# return _map -# def menu(self): -# return self._config['menu'] -# def plugins(self): -# return copy.deepcopy(self._plugins) if 'plugins' in self._config else {} -# def context(self): -# """ -# adding custom variables functions to Jinja2, this function should be called after plugins are loaded -# """ -# _plugins = self.plugins() -# # if not location: -# # env = Environment(loader=BaseLoader()) -# # else: -# location = self._config['layout']['root'] -# # env = Environment(loader=FileSystemLoader(location)) -# env = Environment(loader=BaseLoader()) -# # env.globals['routes'] = _config['plugins'] -# return env -# def config(self): -# return copy.deepcopy(self._config) -# def system(self,skip=[]): -# """ -# :skip keys to ignore in the object ... -# """ -# _data = copy.deepcopy(self._config['system']) -# _system = {} -# if skip and _system: -# for key in _data.keys() : -# if key not in skip : -# _system[key] = _data[key] -# else: -# _system= _data -# return _system -# def layout(self): -# return self._config['layout'] -# def get_app(self): -# return self._config['system']['app'] - - -# class Router : -# def __init__(self,**_args) : - -# # _app = Getter (path = path) -# _app = Getter (**_args) - - -# self._id = 'main' -# # _app.load() -# self._apps = {} -# _system = _app.system() -# if 'routes' in _system : -# _system = _system['routes'] -# for _name in _system : -# _path = _system[_name]['path'] -# self._apps[_name] = Getter(path=_path,caller=_app,location=_path) -# self._apps['main'] = _app - -# def set(self,_id): -# self._id = _id -# def get(self): - -# return self._apps['main'] if self._id not in self._apps else self._apps[self._id] -# def get_main(self): -# return self._apps['main'] diff --git a/cms/engine/plugins/__init__.py b/cms/engine/plugins/__init__.py index de60ce0..b08152f 100644 --- a/cms/engine/plugins/__init__.py +++ b/cms/engine/plugins/__init__.py @@ -5,46 +5,9 @@ import importlib.util import os # -# Defining the decorator to be used in plugins, this will enable simple loading and assigning mimetype to the output (if any) +# let's define the default plugins that will be included into the # - -# def stats (_config) : -# """ -# Returns the statistics of the plugins -# """ -# _data = [] -# for _name in _config : -# _log = {"files":_name,"loaded":len(_config[_name]),"logs":json.dumps(_config[_name])} -# _data.append(_log) -# return pd.DataFrame(_data) -# pass -# def load(_path,_filename,_name) : -# """ -# This function will load external module form a given location and return a pointer to a function in a given module -# :path absolute path of the file (considered plugin) to be loaded -# :name name of the function to be applied -# """ -# # _path = _args['path'] #os.sep.join([_args['root'],'plugin']) - -# if os.path.isdir(_path): -# files = os.listdir(_path) -# if files : -# files = [name for name in files if name.endswith('.py') and name == _filename] -# if files: -# _path = os.sep.join([_path,files[0]]) -# else: -# return None -# else: -# return None -# #-- We have a file ... -# # _name = _args['name'] -# spec = importlib.util.spec_from_file_location(_filename, _path) -# module = importlib.util.module_from_spec(spec) -# spec.loader.exec_module(module) -# # -# # we need to make sure we have the plugin decorator here to make sure - -# return getattr(module,_name) if hasattr(module,_name) else None - - +def authorizationURL (**_args): + # _config = _args['config'] + _source = _args['config']['system']['source'] \ No newline at end of file diff --git a/cms/index.py b/cms/index.py index 8ee4ab1..56f5e63 100644 --- a/cms/index.py +++ b/cms/index.py @@ -1,8 +1,5 @@ -__doc__ = """ - arguments : - --config path of the configuration otherwise it will look for the default in the working directory -""" -from flask import Flask,render_template,send_from_directory,request, redirect, Response, session + +from flask import Flask,render_template,make_response,request, redirect, Response, session import flask #import transport #from transport import providers @@ -19,7 +16,7 @@ import typer from typing_extensions import Annotated from typing import Optional - +import numpy as np import pandas as pd import uuid import datetime @@ -31,27 +28,27 @@ import cms.sites _app = Flask(__name__) cli = typer.Typer() -def _getHandler (app_id,resource=None) : - global _qcms - _id = _getId(app_id,resource) +# def _getHandler (app_id,resource=None) : +# global _qcms +# _id = _getId(app_id,resource) - return _qcms._apps[_id] -def _getId(app_id,app_x): - if app_x not in [None,''] : - _uri = '/'.join([app_id,app_x]) - return _uri[:-1] if _uri.endswith('/') else _uri - return app_id -def _setHandler (app_id,resource) : - session['app_id'] = _getId(app_id,resource) +# return _qcms._apps[_id] +# def _getId(app_id,app_x): +# if app_x not in [None,''] : +# _uri = '/'.join([app_id,app_x]) +# return _uri[:-1] if _uri.endswith('/') else _uri +# return app_id +# def _setHandler (app_id,resource) : +# session['app_id'] = _getId(app_id,resource) @_app.route("/favicon.ico",defaults={'app':None}) @_app.route("//favicon.ico") def favicon (app): global _qcms - _site = _qcms.get (app) - return _read(app,None,_site.get('system.icon')) - - return + _site = _qcms.get (request) + # return _read(app,None,_site.get('system.icon')) + + return redirect(_site.get('system.icon')[1:]) #_site.read(request,uri=_site.get('system.icon')) @_app.route("/<_id>/robots.txt") @_app.route("/robots.txt",defaults={'_id':None}) def robots_txt(_id): @@ -60,7 +57,7 @@ def robots_txt(_id): menu options """ global _qcms - _site = _qcms.get (_id) + _site = _qcms.get (request) _routes = _site.get('system.routes') _context = _site.get('system.context') _info = [f''' @@ -78,84 +75,123 @@ def robots_txt(_id): # return '\n'.join(_info),200,{'Content-Type':'plain/text'} return Response('\n'.join(_info), mimetype='text/plain') -def _getIndex (app_id ,resource=None): - _handler = _getHandler(app_id,resource) - _layout = _handler.layout() - _status_code = 200 - global _qcms - _index_page = 'index.html' - _args = {} +@_app.before_request +def format(): + global _qcms + isroute = _qcms.inspect.isroute(request) + isfile = _qcms.inspect.isfile(request) + isapi = _qcms.inspect.isapi(request) + _map = ['/reload','/version','/dialog','/login','/logout'] + isendpoint = request.path.split('/')[1] in _map or request.path in _map + # print (request.path.split('/'),' ** ',request.path) - try : - _uri = os.sep.join([_layout['root'],_layout['index']]) - _id = _getId(app_id,resource) - _args = _qcms.render(_uri,'index',_id) + if not isendpoint and not isfile and not isapi and not request.path.endswith('/'): - except Exception as e: - _status_code = 404 - _index_page = '404.html' - print(e) - return render_template(_index_page,**_args),_status_code -@_app.route("/") -def _index (): - # return _getIndex('main') + return redirect(request.path+'/') + +@_app.route("/login",methods=['GET'],strict_slashes = False) +def login (): global _qcms - _app = _qcms.get(None) - _uri = os.sep.join([_app.get('layout.root'),_app.get('layout.index')]) - _html = _qcms.render(_uri,'index') + # _secureMethod = request.headers.get('method','pam') + + + # + # are we using a default html document ? + # + _site = _qcms.get(None) + + # _page = f'login/{_site.secure.method()}.html' + _uri = _site.get('layout.login') + # _args= _site.get('system.source.secure') + + _args = {} + _args['login'] = {'model':_site.secure.method()} + _args['system'] = _site.get('system') + _args['layout'] = _site.get('layout') + # + # add addition information associated with authentication + # + if 'source' in _args['system']: + del _args['system']['source'] - return render_template('index.html',**_html),200 #render_template('index.html',**_qcms.render(_uri,'index')),200 + if not _uri : + _uri = f'login/{_site.secure.method()}.html' + _html = render_template(_uri,**_args) + else: + # + # let's place the html content here ... + _html = _site.html(_uri,'login') + # + # we need to see what to _do about the session cookies + # + _args['html'] = _html + return render_template('login/login.html',**_args) -@_app.route("//") -@_app.route("/",defaults={'resource':None}) -def _altIndex(app,resource): +@_app.route("/login",methods=['POST'],strict_slashes = False) +def authenticate () : + # + # we will forward this to the appropriate agent + # global _qcms - _id = _getId(app,resource) - _site = _qcms.get(_id) - _uri = os.sep.join([_site.get('layout.root'),_site.get('layout.index')]) - return render_template('index.html',**_qcms.render(_uri,'index',_id)),200 -@_app.route('/files/',defaults={'app':None,'module':None}) -@_app.route('//files/', defaults=[{'module':None}]) -@_app.route('///files/') -def _read(app,module,file): - global _qcms - _id = _getId(app,module) - _site = _qcms.get(_id) - _stream,_mimeType = _site.read(file) + _site = _qcms.get() + _page = session.get('redirect','') + + _uri = f"{_site.get('system.context')}/{_page}".replace("//","/") + if _uri.endswith('/') : + _uri = _uri[:-1] + + if _site.secure.allow(request=request,uri=_uri) : + return redirect(_uri) + else: + _good,_args = _site.secure.authenticate(request=request) + _site.log(action='authenticate',input={'method':_site.secure.method(),'out':_good}) + if not _good : + uri = request.path + + + if _good : + + _key = list(_good.keys())[0] + response = make_response(redirect(_uri)) + response.set_cookie(_key,_good[_key],**_args) + response.headers['Location'] = _uri + return response + else: + + # + # error 403 must be returned + # + # response = make_response(redirect(_uri)) + # response.status = 403 + # uri = request.path + pass + return redirect(request.path),403 - return io.BytesIO(_stream),200,{'Content-Type':_mimeType} -@_app.route('//dialog') -@_app.route('/dialog',defaults={'app':None}) -def _getdialog(app): +@_app.route("/logout",methods=["POST",'GET'],strict_slashes = False) +def logout(): + global _qcms + _site = _qcms.get() + _id = _site.secure._authContext #-- cookie key + resp = make_response(redirect("/")) + resp.delete_cookie(_id) + return resp + +@_app.route('/dialog',defaults={'app':None},methods=['POST','GET'], strict_slashes = False) +@_app.route('//dialog', methods=['POST','GET'], strict_slashes = False) + +def _dialog(app): global _qcms - _site = _qcms.get(app) + # _site = _qcms.get(app) + _site = _qcms.get(request) _uri = request.headers['uri'] _id = request.headers['dom'] + # _html = ''.join(["
",str( e.render(**_args)),'
']) - _args = _qcms.render(_uri,'html',app) #session.get('app_id','main')) + # _args = _qcms.render(_uri,'html',app) #session.get('app_id','main')) + _args = _site.render(id='html', request=request) _args['title'] = _id return render_template('dialog.html',**_args) #title=_id,html=_html) - -@_app.route("/api//",defaults={'app':None,'key':None},methods=['GET','POST','DELETE','PUT']) -@_app.route("//api//",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) -@_app.route("///api//",methods=['GET','POST','DELETE','PUT']) -def _proxyCall(app,key,module,name): - global _qcms - - _id = _getId(app,key) - - _site = _qcms.get(_id) - _uri = f'api/{module}/{name}' - _delegate = cms.delegate() - return _delegate(uri=_uri, handler=_site, request=request) - -@_app.route('/version') -def _version (): - global _qcms - _site = _qcms.get() - global _config - return _site.get('system.version') -@_app.route("/reload/") +@_app.route("/reload/",strict_slashes = False) def _reload(key) : global _qcms @@ -167,10 +203,53 @@ def _reload(key) : else: return "",403 -@_app.route('/reload',methods=['POST']) +@_app.route('/reload/',methods=['POST'],strict_slashes = False) def reload(): _key = request.headers['key'] if 'key' in request.headers else None return _reload(_key) + +@_app.route("/",defaults={'file':None}, methods=['POST','GET','PUT'], strict_slashes = False) +@_app.route('/',defaults={}, methods=['POST','GET','PUT'], strict_slashes = False) +# @_app.route('//files/', defaults=[{'module':None}]) +# @_app.route('///files/') +def _read(file): + global _qcms + # + # apply security here (on the top level site only) + # @TODO: upon failure, need to forward this to an error page ... + # + _site = _qcms.get(None) + if _site.secure.allow(request=request) : + return _qcms.delegate(request) + else: + session['redirect'] = request.path + return redirect("/login") + + +# @_app.route("/api/", methods=['POST','GET','PUT'], defaults={'uri':None,'route':None}) +# @_app.route("/api/", methods=['POST','GET','PUT']) +# def _api (route,uri) : +# # print () +# pass +# @_app.route("/api//",defaults={'app':None,'key':None},methods=['GET','POST','DELETE','PUT']) +# @_app.route("//api//",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) +# @_app.route("///api//",methods=['GET','POST','DELETE','PUT']) +# def _proxyCall(app,key,module,name): +# global _qcms + +# _id = _getId(app,key) + +# _site = _qcms.get(request) +# _uri = f'api/{module}/{name}' +# _delegate = cms.delegate() +# return _delegate(uri=_uri, handler=_site, request=request) + +@_app.route('/version',strict_slashes = False) +def _version (): + global _qcms + _site = _qcms.get() + global _config + return _site.get('system.version') @_app.route('/page',methods=['POST'],defaults={'app_id':None,'key':None}) @@ -244,3 +323,4 @@ def _help() : pass if __name__ == '__main__' : cli() + diff --git a/cms/meta/__init__.py b/cms/meta/__init__.py index 4e7cd7b..00a634e 100644 --- a/cms/meta/__init__.py +++ b/cms/meta/__init__.py @@ -1,5 +1,5 @@ __author__ = "Steve L. Nyemba" -__version__= "2.4.0" +__version__= "2.4.2" __email__ = "steve@the-phi.com" __license__=""" Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center diff --git a/cms/secure.py b/cms/secure.py new file mode 100644 index 0000000..416b20b --- /dev/null +++ b/cms/secure.py @@ -0,0 +1,174 @@ + +import os +import json +import transport +import plugin_ix +import copy +import uuid +import pandas as pd +import io + + +class Manager : + """ + {secure:{path,registry}} + qcms secure will secure the project (download authentication modules) + """ + def __init__(self,**_args): + # + # let's find the security configuration file + # + _appConfig = _args['config'] + self._config = None + self._regPath = None + self._authKey = None + self._authContext = None + _appContext = _args['config']['system']['context'] + self._loginURI = None + + _csv = """user,uri,allow\n*,*,1""" + + self._permissions = pd.read_csv(io.StringIO(_csv)) + + if 'secure' in _args['config']['system']['source'] : + self._authContext = _appConfig['system']['source']['secure']['id'] + self._path = _appConfig['system']['source']['secure']['path'] + self.inspect(self._path,'authentication configuration') + + if os.path.exists (self._path ) : + _content = (open(self._path )).read() + self._config = json.loads(_content) + self._authKey= self._config['method'] #-- pam, oauth2, nextcloud, ssh + # + # the configuration {method,token} + + + # + # plugins home folder + _path = _appConfig['system']['source']['secure']['registry'] + self.inspect(_path,'plugins installed') + self._plugins = plugin_ix.Registry(folder=_path) + self._plugins.load() + # + # we need to load the permissions here ... + # + _kwargs = _appConfig['system']['source']['secure'] + self._loginURI = [_appConfig['system']['context']] + if 'uri' in _kwargs : + self._loginURI.appendd(_kwargs['uri']) + else: + self._loginURI.append('login') + # if 'authorization' not in _kwargs: + # + # we will assume that all authenticated users have access to every part of the site + # + if 'authorization' in _kwargs: + # + # loading permissions table from a designated location + reader = transport.get.reader(**_kwargs['authorization']) + self._permissions = reader.read() + + + # + # @TODO: add the login page provided int the configuration with full permissions + # or + # self._permissions = pd.concat([pd.DataFrame([{"user":"*","uri":self._uri(),"allow":1}]),self._permissions]) + def inspect (self,path,what) : + if not path or not os.path.exists(path) : + raise Exception (f'Missing {what} {path}') + def authenticate(self,**_args): + print (" ********* ", self._authKey) + _kwargs = copy.copy(_args) + _kwargs['config'] = self._config + # _user = self.login(**_kwargs) + _request = _args['request'] + _method = self.module(f'{self._authKey}.login') + + _user = _method(_request,self._config) + # + # now we have to find a way to create a cookie that has the content of the + + _value = uuid.uuid4().hex + + if not _user : + return {},None + + # + # The additional parameter we can/should add the following + # max_age, secure, httpsonly, domain + # + if 'age' not in self._config : + self._config['age'] = 3600 + return {self._authContext:json.dumps({'token':_value,'username':_user })},{"max_age":self._config['age']} + + + def allow(self,**_args): + """ + The analysis of permissions is based on propositional logic (permission table) to which the policy is applied i.e how we decide to allow access, we opt for the most restrictive approach + """ + _request = _args['request'] + + _stream = _request.cookies.get(self._authContext,None) + + _user = '*' + _uri = _request.path if 'uri' not in _args else _args['uri'] + if _uri.endswith('/') : + _uri = _uri[:-1] + if _stream : + # _object = json.loads(_stream) + # _user = _object['user'] + return 1 + # + # let's extract the matching criteria from the permissions data + # From the matching criteria we will select the most restrictive + _query=f"""(user=='{_user}' or user=='*') and ((uri=='{_uri}' or uri=='*') or allow==1) """ + _values = self._permissions.query(_query).allow.tolist() + if not _values: + # + # + return False + # + # if of the matching criteria we do not have a denial, the system will allow access + # + return 0 not in _values + + # + # we should also look into the cookie + # return _uri not in self._config['permissions'] or self._config['permissions'] == '*' or self._config['uri'] == _uri + def module (self,_name): + """ + We need to identify the location of the plugins + """ + _name = _name.strip() + + if self._plugins.has(_name): + _m = self._plugins.get(_name) + return copy.deepcopy(_m) if _m else None + return None + def method (self): + + return self._config['method'] + # def _uri (self) : + # return self._config.get('uri',None) + def loginURI (self): + return '/'.join(self._loginURI) if self._loginURI else None + + +class parameters: + """ + This is a decorator intended to abstractly handle a plugin + """ + def __init__(self, **_args): + self._input = _args['input'] + self._method = ['POST'] + # self._model = _args['model'] + + def __call__(self, _callback): + def _wrapper(*args, **kwargs): + return _callback(*args, **kwargs) + + setattr(_wrapper, 'input', self._input) + setattr(_wrapper, 'method', self._method) + return _wrapper + + diff --git a/cms/sites/__init__.py b/cms/sites/__init__.py index 5ceff9d..3ae6618 100644 --- a/cms/sites/__init__.py +++ b/cms/sites/__init__.py @@ -3,13 +3,44 @@ This file will describe how a site is loaded """ from cms.engine.config.structure import Layout, System from cms import disk, cloud +import cms import copy import json import io import os from jinja2 import Environment, BaseLoader, FileSystemLoader from datetime import datetime +import mistune +from mistune import markdown +from flask import make_response, render_template +import numpy as np +import pandas as pd +class RequestController : + def __init__(self,_route): + self._routes = _route + def isfile(self,request): + # + # call self.exists to determin if this is an actual file or not + return '.' in request.path + def isroute(self,request): + # return '/'.join(request.path[1:].split('/')[:2]) in self._routes + return self.get_route(request) in self._routes + + return False + def isapi(self,request): + return 'api' in request.path + def get_route(self,request): + _items = request.path[1:].split('/') + _items = _items if _items[-1] != '' else _items[:-1] + N = len(_items) + 1 + _names = [] + for i in range(1, len(_items) + 1): + + _names.append('/'.join(_items[:i])) + + _names=[_item for _item in _names if _item in self._routes] + return _names[-1] if _names else None class IOConfig: """ @@ -18,9 +49,12 @@ class IOConfig: def __init__(self,**_args): self._config = {'system':{},'layout':{},'plugins':{}} self._caller = None - self._location= _args['location'] if 'location' in _args else None + self._location= _args['location'] if 'location' in _args else None + self._logs = [] def get(self,_key) : + # + # @TODO: Include elements to be skipped (just in case & for security reasons) if not _key : return self._config _keys = _key.split('.') if '.' in _key else [_key] @@ -34,6 +68,11 @@ class IOConfig: return _object def set(self,_expr,value): + """ + Set the value of an attribute given the path with dot of the attribute + _expr expression of the attribute e.g: person.age + value value to be set for the attribute + """ if len(_expr.split('.')) == 1 : self._config[_expr] = value else: @@ -62,7 +101,16 @@ class Initialization (IOConfig): # # Invoke initialization self.reload() + # + if self._caller and self._caller.secure : + self.secure = self._caller.secure + else: + self.secure = cms.secure.Manager(config = self._config) + # + # Log initializaton ... + # + self.log(action='init.security',module='site.init',input= self.secure._permissions.to_dict(orient='records')) def reload (self): _args = self._args self._config = self.read_config(**_args) @@ -79,7 +127,9 @@ class Initialization (IOConfig): self.context(**_args) self.menu() - self.plugins() + self.plugins() + + def read_config(self,**_args) : _config = {} if 'path' in _args : @@ -104,7 +154,9 @@ class Initialization (IOConfig): pass _context = self.get('system.context') - + _context = _context[:-1] if _context.endswith('/') else _context + # _context = f'{_context}/' if _context != "" and not _context.endswith("/") else _context + # _context = f'{_context}/files' if self._caller : # # There is a parent context we need to account and we are updating the current context to reflect the caller context @@ -114,7 +166,6 @@ class Initialization (IOConfig): # updating the configuratioin _iconURI = '/'.join(["",_parentContext,self._caller.get('system.icon')]) - # print ([self._caller.get('system.context'), _parentContext,_context]) if self._caller.get('system.context') != '': _parentContext = "/"+_parentContext _context = "/"+_context @@ -162,13 +213,14 @@ class Initialization (IOConfig): _context = self.get('system.context') _logo = self.get('system.logo') _root = self.get('layout.root') - if self.get('system.source.id') == 'cloud' : - _icon = f'{_context}/api/cloud/download?doc={_logo}' - else: - _icon = f'{_context}/api/disk/read?uri={_logo}' - # _root = f'{_context}/api/disk/read?uri={_root}' - - # self.set('layout.root',_root) + # if self.get('system.source.id') == 'cloud' : + # _icon = f'{_context}/api/cloud/download?doc={_logo}' + # else: + # _icon = f'{_context}/api/disk/read?uri={_logo}' + _icon = f'{_context}/{_logo}'.replace(_root,'') + + + self.set('system.icon',_icon) self.set('system.logo',_icon) # @@ -215,10 +267,6 @@ class Initialization (IOConfig): # # At this point the entire menu is build and we need to have it sorted self.order() - # _labels = list(self.get('layout.menu').keys()) - # self.log(action='init.menu',module='menu',input=_labels) - # print (self.get('layout.menu')) - # print (_object) def order(self,**_args): if self.get('layout.order.menu') : _sorted = {} @@ -269,12 +317,14 @@ class Initialization (IOConfig): _parentContext = self.get('system.parentContext') _map = {} _plugins = {} - if self.get('system.source.id') == 'cloud' : - _plugins = cloud.plugins(_context) - else: - _plugins = disk.plugins(context=_context) - _uri = 'api/system/debug' - _uri = _uri if not _context else f'{_context}/{_uri}' + # if self.get('system.source.id') == 'cloud' : + # _plugins = cloud.plugins(_context) + # else: + # _plugins = disk.plugins(context=_context) + _uri = f'{_context}/api/system/debug' + if _uri.startswith('/') : + _uri = _uri[1:] + # _uri = _uri if not _context else f'{_context}/{_uri}' _plugins[_uri] = self.debug if os.path.exists(_folder) and self.get('plugins'): @@ -289,6 +339,9 @@ class Initialization (IOConfig): if _pointer : _uri = f"api/{_filename}/{_module}" _uri = f"{_context}/{_uri}" if _context else _uri + if (_uri.startswith("/")) : + _uri = _uri[1:] + _map[_uri] = _pointer if _parentContext : # _uri = f"{_parentContext}/{_context}" @@ -306,7 +359,9 @@ class Initialization (IOConfig): # Updating plugins from disk/cloud _plugins = _map if not _plugins else dict(_plugins,**_map) # + # if we have login enabled we should add them as a plugins # + self.set('plugins',_plugins) self.log(action='init.plugins',module='plugins',input=list(_plugins.keys())) @@ -314,39 +369,274 @@ class Site(Initialization) : def __init__(self,**_args): super().__init__(**_args) self._config['system']['portal'] = (self.get('system.routes')) == None - def html(self,_uri,_id) : - _handler = cloud if self.get('system.source.id') == 'cloud' else disk - _html = _handler.html(_uri, self.get(None)) - return " ".join([f'
',_html,"
"]) - def read(self,_uri) : + self._routes = [] + if self.get('system.routes') : + self._routes = list(self.get('system.routes').keys()) + if self._caller : + self._routes = self._caller._routes + self.inspect = RequestController(self._routes) + # + # let's update the plugins + # + if self.secure.method() in ['oauth2.0','oauth20','oauth2'] : + _plugins = self.get('plugins') + _plugins['api/oauth2/authorize'] = cms.authorizationURL + _plugins['api/oauth2/final'] = cms.oauthFinalize + self.set('plugins',_plugins) + def exists (self,uri): + path = [] + if self.get('layout.location'): + path.append(self.get('layout.location')) + if self.get('layout.root') not in uri : + path.append(self.get('layout.root')) + + path.append(uri) + return os.path.exists( os.sep.join(path)) + + # def _html(self,_uri,_id,request) : + # if self.secure.allow(request=request): + # _handler = cloud if self.get('system.source.id') == 'cloud' else disk + # _html = _handler.html(_uri, self.get(None)) + # # + # # maybe apply the environment here ?? + # return " ".join([f'
',_html,"
"]) + # else: + # None + def mimeType(self,uri): + _extension = uri.split('.')[-1].strip().lower() + _mimeType = 'application/octet-stream' + + + if _extension in ['css','js','csv','html','md'] : + _mimeType = f'text/{_extension}'.replace('md','html') + if _extension == 'js' : + _mimeType = 'text/javascript' + + elif _extension in ['png','jpg','jpeg'] : + _mimeType = f'image/{_extension}' + return _mimeType + def path(self,_uri): + path = [] + + if self.get('layout.location'): + path.append(self.get('layout.location')) + if self.get('layout.root') not in _uri : + path.append(self.get('layout.root')) + + path.append(_uri) + return os.sep.join(path) + def html (self,_request): + _uri = self.uri(_request) + _mimeType = self.mimeType(_uri) + f = open(self.path(_uri),'r') + _content = f.read() #_handler.html(_uri, self.get(None)) + f.close() + + + if 'md' in _mimeType or 'html' in _mimeType : + # _content = f'
{_content}
' + if _uri.split('.')[-1].strip().lower() == 'md': + _content = mistune.html(_content).replace(""",'"').replace("<","<").replace(">",">") if _uri[-2:] in ['md','MD','Md','mD'] else _content + _content = f"
{_content}
" #if 'dom' not in _request.headers else f'
{_content}
' + else: + _env = Environment(loader=BaseLoader()).from_string(_content) + _content = str(_env.render(**self.get(None))) + pass + return _content,_mimeType + def uri(self,request): + if 'uri' in request.headers or 'uri' in request.args : + return request.headers['uri'] if 'uri' in request.headers else request.args.get('uri',None) + else: + _route = self.inspect.get_route(request) + # + # The route doesn't have any forward slashes nor slash at the end + # + file = request.path.replace(f'{_route}','') + # + if file.startswith('//') or file.startswith('/') : + # NOTE: false positive if file.startswith('/') is used + file = file[2:] if file.startswith('//') else file[1:] + + + return file if file.strip() not in ['',None,'/'] else self.get('layout.index') + + # # file = '/'.join(request.path[1:].split('/')[1:]) + + # file = request.path[1:] + # _isapi = 'api' in request.path + # _isroute = '/'.join(request.path[1:].split('/')[:2]) in self._routes + # _isfile = '.' in request.path + + # return file if _isfile and not _isroute else self.get('layout.index') + + def read(self,request) : + _kwargs = {'allow':0} + # if self.secure.allow(request=request): + + _uri = self.uri(request) _handler = cloud if self.get('system.source.id') == 'cloud' else disk - return _handler.read(uri=_uri, config=self.get(None)) + _extension = _uri.split('.')[1].lower() + _mimeType = self.mimeType(_uri) + + if _extension.strip() in ['md','txt','html','js','css'] : + _content,_mimeType = self.html(request) + else: + # + # Opening a binary file + + f = open(self.path(_uri),'rb') + _content = io.BytesIO(f.read()) + + f.close() + # _content,_ = _handler.read(uri=_uri, config=self.get(None)) + _kwargs = {'allow':1,'mimeType':_mimeType,'extension':_extension, + 'path':self.path(_uri), + 'uri':_uri,'request':request.path} + self.log(action='file.read',module='site.read',input=_kwargs) + if _content : + return _content, _mimeType + return None, 'plain/html' + # if 'html' in _mimetype : + + # _args = {'layout':self.get('layout'),'system':self.get('system')} + # _content = _content.decode('utf-8') if type(_content) == bytes else _content + # _env = Environment(loader=BaseLoader()).from_string(_content) + # _content = _env.render(**_args) + # return _content,_mimetype + # return None,'plain/html' + def render (self,**_args): + """ + :id target of the dom or jinja + :request incoming request + :uri uri to read from disk ... + """ + # _site = self._sites[_appid] if _appid else self._sites[self._id] + _id = _args.get('id') + # _uri= _args['uri'] + _request = _args['request'] + + _uri = self.uri(_request) + + _kwargs = {'layout':self.get('layout')} + _system = self.get('system') + for k in ['source','app'] : + if k in _system : + del _system[k] + _kwargs['system'] = _system + _cookies = json.loads(_request.cookies.get(self.secure._authContext ,"{}")) + + if _cookies : + _kwargs['username'] = _cookies['username'] + _html,_mimeType = self.html(_request) + + # _kwargs[_id] = f'
{_html}
' + # return _kwargs + # _html = f'
{_html}
' + if _html : + _env = Environment(loader=BaseLoader()).from_string(f'
{_html}
') + _kwargs[_id] = str(_env.render(**_kwargs)) + return _kwargs + return None + def apply_tags (self,_html): + _kwargs = {'layout':self.get('layout'),'system':self.get('system')} + _env = Environment(loader=BaseLoader()).from_string(f'
{_html}
') + return _env.render(**_kwargs) + def run(self,_request) : + _plugins = self.get('plugins') + _data = "

404

" + _mimeType = 'plain/html' + _code = 404 + _key = _request.path[1:] #if self.get('system.context') != '' else _request.path[1:] + # print ([_key,_key in list(_plugins.keys())]) + # print (list (_plugins.keys())) + if _plugins and _key in _plugins: + _mimeType = 'application/octet-stream' + _pointer = _plugins.get(_key) + if hasattr(_pointer,'mimetype'): + _mimeType = _pointer.mimetype + _data = _pointer(request=_request,config=self.get(None)) + if hasattr(_pointer,'method') and _request.method not in _pointer.method : + _data = "

404

" + + elif not hasattr(_pointer,'mimetype'): + _data,_mimeType = _pointer(request=_request,config=self.get(None)) + if 'html' in _mimeType : + _data = self.apply_tags(_data) + if type(_data) == pd.DataFrame : + _data = _data.to_json(orient='records') + + elif type(_data) in [dict,list] : + _data = json.dumps(_data) + # # + # # return the ata + _code = _code if not _data else 200 + # resp = make_response(_data) + # resp.status_code = _code + # resp.headers['Content-Type'] = _mimeType + # resp.headers['Content-Disposition'] = 'inline' + return _data,_code,{"Content-Type":_mimeType} class QCMS: def __init__(self,**_args): _app = Site(**_args) self._id = _app.get('system.context') #if _app.get('system.context') else 'main' - self._sites = {self._id:_app} + # if self._id == '' : + # self._id = '/' + + self._sites = {self._id:_app, '/':_app} + self._routes = [] if _app.get('system.routes') : _routes = _app.get('system.routes') for _name in _routes : + self._routes.append(_name) _path = _routes[_name]['path'] self._sites[_name] = Site(context=_name,path=_path,caller=_app) - def render(self,_uri,_id,_appid=None): - _site = self._sites[_appid] if _appid else self._sites[self._id] - _args = {'layout':_site.get('layout')} - _system = _site.get('system') - for k in ['source','app'] : - if k in _system : - del _system[k] - _args['system'] = _system - _html = _site.html(_uri,_id) + # self._sites[f'{_name}/'] = self._sites[_name] + - _env = Environment(loader=BaseLoader()).from_string(_html) - _args[_id] = str(_env.render(**_args)) - return _args + self.inspect = RequestController(self._routes) + + def _render(self,request): + _site = self.get(request) + _args = _site.render(request=request,id='index') + return render_template('index.html',**_args) + def _read(self,request): + _site = self.get(request) + return _site.read(request) + def delegate(self,request): + isfile = self.inspect.isfile(request) + isapi = self.inspect.isapi(request) + isroute= self.inspect.isroute(request) + if not isapi and not isfile: + return self._render(request) + # + # # let's check on files to be services (assumption is that they should have an extension) + # The following propositional logic is as such + # (isfile and isroute) or (isfile and not isroute) -> isfile (isroute is optional) + if isfile : + _content,_mimeType = self._read(request) + return _content,200,{'Content-Type':_mimeType} + if isapi : + _site = self.get(request) + return _site.run(request) + + def get(self,request=None) : + if request : + if self.inspect.isroute(request) : + self._id = self.inspect.get_route(request) + else: + self._id = '' + else: + self._id = '' + return self._sites[self._id] def set(self,_id): self._id = _id - def get(self,_id=None): - return self._sites[self._id] if not _id else self._sites[_id] + def allow(self,id, request) : + _site = self.get(id) + + return _site.secure.allow(request=request) + def has(self,_id): + return _id in self._sites + + diff --git a/cms/static/css/qcms-login.css b/cms/static/css/qcms-login.css new file mode 100644 index 0000000..9f29d3a --- /dev/null +++ b/cms/static/css/qcms-login.css @@ -0,0 +1,28 @@ + .qcms-login {display:grid; grid-template-rows: 64px auto 64px; gap:4px; padding:8px} + .qcms-login .border {border:4px solid #CAD5E0} + + .qcms-login .header {display:grid; grid-template-columns: 80px auto; align-items: center;} + .qcms-login .header img{ height:64px; margin:4px;} + .qcms-login-buttons {display:grid; + grid-template-columns: 50% 50%; gap:4px; + width:100%; + align-items:center; + + } + .qcms-login-buttons .fa-times {color:maroon} + .qcms-login-input { + display:grid; + grid-template-rows: 64px 64px; gap:4px; + } + .qcms-login-input INPUT{ + padding:8px; + border:4px solid transparent; + background-color: #f3f3f3; + + + outline: 0; + } + .qcms-login-input INPUT:focus { + border-left-color: #4682B4;; + } + .qcms-login .small {font-weight: lighter; font-size:11px} \ No newline at end of file diff --git a/cms/static/js/qcms/menu.js b/cms/static/js/qcms/menu.js index 95bdf19..8e19192 100644 --- a/cms/static/js/qcms/menu.js +++ b/cms/static/js/qcms/menu.js @@ -14,10 +14,14 @@ qcms.menu.Common = function (){ var http = HttpClient.instance() http.setHeader('dom',_domId) http.setHeader('uri',uri) - http.post(`${qcms.context}/page`,(x)=>{ + http.post(`${qcms.context}/${uri}`,(x)=>{ // // @TODO: In case of an error + var _dom = $(x.responseText) + if ( $(_dom).attr("id") == null){ + $(_dom).attr("id",_domId) + } if($(`${_parentId} #${_domId}`).length){ $(`${_parentId} #${_domId}`).remove() } @@ -59,6 +63,7 @@ qcms.menu.Basic = function (_layout,_outputId,_domId){ // var _finalize = this._finalize // var _domId = this._domId var _me = this ; + _items.forEach(_item=>{ var _div = jx.dom.get .instance('DIV') @@ -113,8 +118,9 @@ qcms.menu.Basic = function (_layout,_outputId,_domId){ */ var _names = _layout.order.menu.length > 0 ? _layout.order.menu : Object.keys(_layout.menu) _names.forEach ((_name)=>{ - var _div = this._build(_layout.menu[_name]) ; - + if (_layout.menu[_name]){ + + var _div = this._build(_layout.menu[_name]) ; var _sub = jx.dom.get.instance('DIV') var _menuItem = jx.dom.get.instance('DIV') @@ -126,7 +132,7 @@ qcms.menu.Basic = function (_layout,_outputId,_domId){ _menuItem.appendChild(_sub) _domId = (_domId == null || $(_domId).length == 0)?'.main .menu' : _domId $(`${_domId}`).append(_menuItem) - + } }) } diff --git a/cms/static/js/qcms/page-loader.js b/cms/static/js/qcms/page-loader.js index b778e7b..cf0a872 100644 --- a/cms/static/js/qcms/page-loader.js +++ b/cms/static/js/qcms/page-loader.js @@ -5,7 +5,12 @@ qcms.page = {} qcms.page.Observer = function (_parentId,_domId,_uri){ this._parentId = _parentId == null?'':_parentId this._domId = _domId + this._uri = _uri + if (qcms.root){ + //-- bug fix for some adjustments that were needed for streamlined version + this._uri = this._uri.replace(`${qcms.root}/`,'') + } this.finalize = function(_id){ var _script = $(`${_id} script`) @@ -26,11 +31,12 @@ qcms.page.Observer = function (_parentId,_domId,_uri){ var http = HttpClient.instance() http.setHeader('dom',this._domId) - http.setHeader('uri',this._uri) - var _uri = this._uri - http.post(`${qcms.context}/page`,function(x){ - + // http.setHeader('uri',this._uri) + var _uri = `${qcms.context}/${this._uri}` + + http.post(_uri,function(x){ var _dom = $(x.responseText) + var _found = qcms.html.hasNode( $(_id), $(_dom)) if (_found == 0){ @@ -51,7 +57,7 @@ qcms.page.Observer = function (_parentId,_domId,_uri){ } qcms.page.loader = function(_parentId,_layout){ - + if (_parentId.constructor == Object) { _layout = _parentId _parentId = null diff --git a/cms/templates/404.html b/cms/templates/404.html index a1d551c..0bb7595 100644 --- a/cms/templates/404.html +++ b/cms/templates/404.html @@ -23,18 +23,8 @@ Vanderbilt University Medical Center - - - - - - - - - - - - + {%include "libs.html" %} + -
-
- {%include "header.html" %} + + + + diff --git a/cms/templates/500.html b/cms/templates/500.html new file mode 100644 index 0000000..bda44ae --- /dev/null +++ b/cms/templates/500.html @@ -0,0 +1,69 @@ + + + + + {{layout.header.title}} + + + + + + + + {%include "libs.html" %} + + + + + + + + + diff --git a/cms/templates/index.html b/cms/templates/index.html index 7a41cf8..5d2ab29 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -15,7 +15,7 @@ Vanderbilt University Medical Center - + {% include "libs.html"%} {{layout.header.title}} @@ -24,55 +24,7 @@ Vanderbilt University Medical Center - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cms/templates/login/login.html b/cms/templates/login/login.html new file mode 100644 index 0000000..2c5b54a --- /dev/null +++ b/cms/templates/login/login.html @@ -0,0 +1,32 @@ +{{layout.header.title}} - Authentication + +{%include "libs.html" %} + + +
+ {{html|safe}} + +
\ No newline at end of file diff --git a/cms/templates/login/nextcloud.html b/cms/templates/login/nextcloud.html new file mode 100644 index 0000000..d846586 --- /dev/null +++ b/cms/templates/login/nextcloud.html @@ -0,0 +1,24 @@ + +{%include "libs.html" %} + + \ No newline at end of file diff --git a/cms/templates/login/oauth2.html b/cms/templates/login/oauth2.html new file mode 100644 index 0000000..974661c --- /dev/null +++ b/cms/templates/login/oauth2.html @@ -0,0 +1,71 @@ +{%include "libs.html" %} + + diff --git a/cms/templates/login/pam.html b/cms/templates/login/pam.html new file mode 100644 index 0000000..7c63003 --- /dev/null +++ b/cms/templates/login/pam.html @@ -0,0 +1,19 @@ +{%include "libs.html" %} + + diff --git a/cms/templates/user.html b/cms/templates/user.html new file mode 100644 index 0000000..7e9b929 --- /dev/null +++ b/cms/templates/user.html @@ -0,0 +1,7 @@ +
+
Hi, {{username}}
+
+
+ +
+
\ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 01087a8..8912e60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,12 +13,12 @@ readme = "README.md" license = {text = "LICENSE"} keywords = ["cms", "www", "https", "flask", "data-transport"] classifiers = [ - "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: MIT License", "Topic :: Utilities", ] dependencies = [ "flask", - "gitpython", + "gitpython", "flask-jwt-extended", "termcolor", "flask-session", "mistune","plugin-ix@git+https://github.com/lnyemba/plugins-ix", @@ -41,10 +41,10 @@ Homepage = "https://healthcareio.the-phi.com/git/code/transport.git" [tool.setuptools] include-package-data = true zip-safe = false -script-files = ["bin/qcms"] +script-files = ["bin/qcms","bin/qcms.bat"] [tool.setuptools.packages.find] -include = ["meta","meta.*", "cms", "cms.*"] +include = ["cms", "cms.*"] [tool.setuptools.dynamic] version = {attr = "cms.meta.__version__"}