mirror of http://localhost:9400/cloud/cms
parent
81a72b882a
commit
bc84ad79d5
@ -0,0 +1 @@
|
||||
from . import plugins, secure, auth
|
||||
@ -0,0 +1,85 @@
|
||||
import typer
|
||||
from typing_extensions import Annotated
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
import os
|
||||
import json
|
||||
import plugin_ix as px
|
||||
from enum import Enum
|
||||
import pandas as pd
|
||||
from rich.table import Table
|
||||
from rich import print
|
||||
import uuid
|
||||
import cms
|
||||
import requests
|
||||
|
||||
|
||||
FAILED = '[ [red] \u2717 [/red] ]'
|
||||
PASSED = '[ [green] \u2713 [/green] ]'
|
||||
cli = typer.Typer()
|
||||
@cli.command(name="set")
|
||||
def set_auth (
|
||||
id:Annotated[str,typer.Argument(help="identifier used in setting cookies")],
|
||||
manifest:Annotated[str,typer.Argument(help="path manifest or site folder")],
|
||||
registry:Annotated[str,typer.Argument(help="path to the registry created by plugin-ix")],
|
||||
authpath:Annotated[str,typer.Argument(help="Authentication file location, that contains the authentication model")]
|
||||
):
|
||||
"""
|
||||
This function will set login configuration to a manifest
|
||||
"""
|
||||
try:
|
||||
_args = {"id":id,"registry":registry,"path":authpath,"authorization":{}}
|
||||
path = cms.engine.config.get_manifest(manifest=manifest)
|
||||
_config = cms.engine.config.get(path)
|
||||
_config['system']['source'] = {'secure':_args}
|
||||
_secEngine = cms.secure.Manager(config=_config)
|
||||
cms.engine.config.write(config=_config,path=path)
|
||||
except Exception as e:
|
||||
print (f"""{FAILED} [bold][red]{id}, failed [/red][/bold], error found {str(e)}""")
|
||||
#
|
||||
# we should try to test this configuration to see if it works
|
||||
#
|
||||
|
||||
pass
|
||||
def permissions():
|
||||
pass
|
||||
def inspect ():
|
||||
pass
|
||||
@cli.command("drop")
|
||||
def remove (manifest:Annotated[str,typer.Argument(help="path manifest or site folder")]):
|
||||
"""
|
||||
This function removes login configuration from a manifest
|
||||
"""
|
||||
path = cms.engine.config.get_manifest(manifest=manifest)
|
||||
_config = cms.engine.config.get(path)
|
||||
_msg = f'{FAILED}'
|
||||
try:
|
||||
if 'secure' in _config['system']['source'] :
|
||||
del _config['system']['source']['secure']
|
||||
cms.engine.config.write(config=_config,path=path)
|
||||
_msg = f'{PASSED} [green]Successfully[/green] removed secure attribute'
|
||||
except Exception as e:
|
||||
_msg = f'{_msg}, error found {str(e)}'
|
||||
|
||||
print (_msg)
|
||||
|
||||
pass
|
||||
@cli.command(name="template")
|
||||
def gen_auth (
|
||||
_model:Annotated[str,typer.Argument(help="generate an authentication parameter file")],
|
||||
):
|
||||
"""
|
||||
This function generates a template login file content for a model (pam, oauth2, nextcloud)
|
||||
"""
|
||||
_conf = {"method":_model}
|
||||
_map = {"pam":{"age":3600},"nextcloud":{"url":"https://your-nextcloud-site"}}
|
||||
#
|
||||
# parameters client_id, client-secret,
|
||||
_map["oauth2"] = {"client_id":"client_id","client_secret":"client_secret","scope":"profile","response_type":"code","authorization_url":"authorization_url","redirect_uri":"redirect_uri"}
|
||||
if _model in _map :
|
||||
_conf = dict(_conf, **_map.get(_model))
|
||||
print (_conf)
|
||||
print ()
|
||||
print(f'{PASSED} [bold] Edit[/bold] and [bold]save[/bold] the content into an [bold]"authentication file"[/bold]')
|
||||
else:
|
||||
print (f"""{FAILED} [bold]{_model}[/bold] is not a supported authentication model""")
|
||||
@ -0,0 +1,207 @@
|
||||
import typer
|
||||
from typing_extensions import Annotated
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
import os
|
||||
import json
|
||||
import plugin_ix as px
|
||||
from enum import Enum
|
||||
import pandas as pd
|
||||
from rich.table import Table
|
||||
from rich import print
|
||||
import cms
|
||||
|
||||
|
||||
FAILED = '[ [red] \u2717 [/red] ]'
|
||||
PASSED = '[ [green] \u2713 [/green] ]'
|
||||
|
||||
"""
|
||||
Handling cli interface for plugins, performing add list and remove
|
||||
"""
|
||||
|
||||
cli = typer.Typer()
|
||||
def to_Table(df: pd.DataFrame):
|
||||
"""Displays a Pandas DataFrame as a rich table."""
|
||||
table = Table(show_header=True, header_style="bold magenta")
|
||||
|
||||
for col in df.columns:
|
||||
table.add_column(col)
|
||||
|
||||
for _, row in df.iterrows():
|
||||
table.add_row(*row.astype(str).tolist())
|
||||
|
||||
# console.print(table)
|
||||
return table
|
||||
|
||||
|
||||
# def cms.get_manifest (manifest):
|
||||
# if not manifest.endswith('json') and os.path.isdir(manifest):
|
||||
# manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1])
|
||||
# path = os.sep.join([manifest,'qcms-manifest.json'])
|
||||
# else:
|
||||
# path = manifest
|
||||
# return path
|
||||
|
||||
# def get_config(path,key=None):
|
||||
|
||||
# f = open(path)
|
||||
# _config = json.loads(f.read())
|
||||
# f.close()
|
||||
# return _config[key] if key and key in _config else _config
|
||||
|
||||
def format(file,_content):
|
||||
return [{'file':file.replace('.py',''),'uri':f'api/{file.replace(".py","")}/{_name}','endpoint':_name} for _name in _content]
|
||||
|
||||
def get (manifest:Annotated[str,typer.Argument(help="project folder or manifest file")],):
|
||||
"""
|
||||
List the plugins in the from a project folder (from the manifest)
|
||||
"""
|
||||
path = cms.engine.config.get_manifest(manifest=manifest) #
|
||||
_config = cms.engine.config.get(path) #get_config(path)
|
||||
|
||||
|
||||
_root = _config['layout']['root']
|
||||
#
|
||||
# remove the manifest file to pull the plugins folder
|
||||
folder = os.sep.join([path.replace('qcms-manifest.json','')[:-1],f'{_root}/_plugins'])
|
||||
loader = px.Loader ()
|
||||
|
||||
_api = []
|
||||
for _name in os.listdir(folder):
|
||||
|
||||
_file = os.sep.join([folder,_name])
|
||||
try:
|
||||
loader.load(file=_file)
|
||||
_api+= format(_name,loader.names())
|
||||
# _ondisk[_name] = loader.names()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
#
|
||||
# If the file has an error, it will be skipped
|
||||
print (e)
|
||||
break
|
||||
#
|
||||
# at this point we know what we have on disk, we should be able to make a report/dashboard of sorts
|
||||
pass
|
||||
|
||||
# On disk
|
||||
# files = os.listdir(folder)
|
||||
#
|
||||
_online = []
|
||||
_plugins = _config['plugins']
|
||||
for _name in _plugins :
|
||||
_online += format(_name, _plugins[_name])
|
||||
return _api,_online
|
||||
|
||||
class StatusFilter(str,Enum):
|
||||
online="online"
|
||||
offline="offline"
|
||||
|
||||
|
||||
@cli.command(name="status")
|
||||
def status(manifest:Annotated[str,typer.Argument(help="project folder or manifest file")],
|
||||
filter : StatusFilter = None
|
||||
):
|
||||
"""
|
||||
This function will provide the status of all available plugins
|
||||
"""
|
||||
_avail,_depl = get(manifest)
|
||||
_avail = pd.DataFrame(_avail)# available on disk (not deployed)
|
||||
_depl = pd.DataFrame(_depl) # deployed
|
||||
files = _avail.file.unique().tolist()+ _depl.file.unique().tolist()
|
||||
_df = pd.DataFrame()
|
||||
for _name in files :
|
||||
_xe = _avail[_avail.file == _name].endpoint.tolist()
|
||||
_ye = _depl[_depl.file == _name].endpoint.tolist()
|
||||
_online = list(set(_xe) & set(_ye))
|
||||
_offline = list(set(_xe) - set(_ye))
|
||||
_data = pd.DataFrame()
|
||||
|
||||
|
||||
_data['api'] = _offline + _online
|
||||
_data['file'] = _name
|
||||
_data['status'] = (['[red]offline[/red]']* len(_offline)) + (['[green]online[/green]']* len(_online))
|
||||
# _data['deployed'] = _online
|
||||
_data = _data[['file','api','status']]
|
||||
_df = pd.concat([_df, _data])
|
||||
# _df.append({'file':_name,'deployed':_online,'offline':_offline})
|
||||
_df['uri'] = _df.apply(lambda row: f'api/{row.file}/{row.api}',axis=1)
|
||||
_df = _df[['file','uri','api','status']]
|
||||
if filter :
|
||||
print (f"status in ('{filter.value}')")
|
||||
_df = _df[_df.status.str.contains(filter.value, case=False, regex=True)]
|
||||
# _df = _df.query(f" '{filter.value}' in status")
|
||||
print (to_Table(_df))
|
||||
pass
|
||||
|
||||
@cli.command(name="register")
|
||||
def add (manifest:Annotated[str,typer.Argument(help="project folder or manifest file")],
|
||||
pointer:Annotated[str,typer.Argument(help="file/function or file.function with no file extension. e.g: demo/info")]
|
||||
):
|
||||
"""
|
||||
This function will add a plugin function to the site's configuration file making it available as an API
|
||||
"""
|
||||
#
|
||||
# Let's make sure we are adding to the configuration something that actually exists
|
||||
#
|
||||
_file,_name = pointer.split('/')
|
||||
path = cms.engine.config.get_manifest(manifest=manifest) #
|
||||
_config = cms.engine.config.get(path) #get_config(path)
|
||||
_root = _config['layout']['root']
|
||||
|
||||
_folder = os.sep.join([path.replace('qcms-manifest.json','')[:-1],f'{_root}/_plugins'])
|
||||
if f'{_file}.py' in os.listdir(_folder) :
|
||||
loader = px.Loader ()
|
||||
loader.load(file=f'{_folder}{os.sep}{_file}.py')
|
||||
if loader.has(_name) :
|
||||
#
|
||||
# adding to plugins
|
||||
_plugins = _config['plugins']
|
||||
if _file not in _plugins :
|
||||
_plugins[_file] = []
|
||||
if _name not in _plugins[_file] :
|
||||
_plugins[_file].append (_name)
|
||||
_config['plugins'] = _plugins
|
||||
cms.engine.config.write(config=_config,path=path)
|
||||
_msg = f"{PASSED} [bold]{pointer}[/bold] successfully added to {manifest}\nAPI endpoint [bold]api/{_file}/{_name}[/bold]"
|
||||
else:
|
||||
_msg = f"{FAILED} [bold]{pointer}[/bold] [red]already exists[/red] in configuration"
|
||||
else:
|
||||
#
|
||||
# failure at this point, asking for a function in a file that doesn't exist
|
||||
_msg = f"[bold]{pointer}[/bold] [red]{_name} missing[/red] in {_folder}{os.sep}{_file}.py"
|
||||
pass
|
||||
else:
|
||||
#
|
||||
# throw/raise an exception
|
||||
_msg = f"{FAILED} [bold]{pointer}[/bold] [red]NOT found[/red] in {_folder}"
|
||||
|
||||
print (_msg)
|
||||
pass
|
||||
@cli.command(name="unregister")
|
||||
def remove(manifest:Annotated[str,typer.Argument(help="project folder or manifest file")],
|
||||
pointer:Annotated[str,typer.Argument(help="file/function or file.function with no file extension. e.g: demo/info")]
|
||||
):
|
||||
"""
|
||||
This function will remove an api from the configuration of a project and won't be available via http/https
|
||||
"""
|
||||
_file,_name = pointer.split('/')
|
||||
path = cms.engine.config.get_manifest(manifest=manifest) #
|
||||
_config = cms.engine.config.get(path) #get_config(path)
|
||||
_root = _config['layout']['root']
|
||||
_plugins = _config.get('plugins',{})
|
||||
if _file in _plugins :
|
||||
_plugins[_file] = [_fname for _fname in _plugins[_file] if _fname != _name]
|
||||
_config['plugins'] = _plugins
|
||||
_msg = f"{PASSED} [bold]{pointer}[/bold] was [green]successfully[/green] removed from {path}"
|
||||
else:
|
||||
_msg = f"{FAILED} [bold]{pointer}[/bold] was [red]NOT found[/red] in {path}"
|
||||
print (_msg)
|
||||
cms.engine.config.write(_config,path)
|
||||
#
|
||||
#
|
||||
# def post(config,path):
|
||||
# f = open(path,'w')
|
||||
# f.write(json.dumps(config,indent=4))
|
||||
# f.close()
|
||||
@ -0,0 +1,88 @@
|
||||
"""
|
||||
This file will handle security aspects associated with QCMS
|
||||
"""
|
||||
|
||||
import typer
|
||||
from typing_extensions import Annotated
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
import os
|
||||
import json
|
||||
import plugin_ix as px
|
||||
from enum import Enum
|
||||
import pandas as pd
|
||||
from rich.table import Table
|
||||
from rich import print
|
||||
import uuid
|
||||
import cms
|
||||
import requests
|
||||
|
||||
|
||||
FAILED = '[ [red] \u2717 [/red] ]'
|
||||
PASSED = '[ [green] \u2713 [/green] ]'
|
||||
cli = typer.Typer()
|
||||
|
||||
|
||||
@cli.command(name="set-key")
|
||||
def set_key (manifest:Annotated[str,typer.Argument(help="path to manifest or manifest folder")],
|
||||
keyfile:Annotated[str,typer.Argument(help="path of the key file to generate")]
|
||||
):
|
||||
"""
|
||||
force-reload of an application
|
||||
"""
|
||||
keyfile = cms.engine.config.get_manifest(keyfile)
|
||||
if not os.path.exists(keyfile):
|
||||
f = open(keyfile,'w')
|
||||
f.write(str(uuid.uuid4()))
|
||||
f.close()
|
||||
#
|
||||
manifest = cms.engine.config.get_manifest(manifest)
|
||||
_config = cms.engine.config.get(manifest)
|
||||
if 'source' not in _config['system']:
|
||||
_config['system']['source'] = {'id':'disk'}
|
||||
_config['system']['source']['key'] = os.path.abspath(keyfile)
|
||||
cms.engine.config.write(_config,manifest)
|
||||
_msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold] : A key was generated and written to {keyfile}
|
||||
use this key in header to enable reload of the site ...
|
||||
"""
|
||||
else:
|
||||
_msg = f"""{FAILED} [bold]{_config['system']['layout']['header']['title']}[/bold] : could [bold]NOT[/bold] generate a key, because it would seem you already have one
|
||||
Please manually delete {keyfile}
|
||||
|
||||
|
||||
"""
|
||||
print (_msg)
|
||||
@cli.command (name='reload')
|
||||
def reload (
|
||||
path:Annotated[str,typer.Argument(help="")],
|
||||
port:int=typer.Option(default=None,help="port of the host to call")
|
||||
) :
|
||||
"""
|
||||
Reload a site/portal given the manifest ...
|
||||
"""
|
||||
path = cms.engine.config.get_manifest(path)
|
||||
_config = cms.engine.config.get( path)
|
||||
if 'source' in _config['system'] and 'key' in _config['system']['source'] :
|
||||
_spath = _config['system']['source']['key']
|
||||
# f = open(_config['system']['source']['key'])
|
||||
if not os.path.exists(_spath) :
|
||||
mpath = path.split(os.sep)[:-1] + _spath.split(os.sep)
|
||||
_spath = os.sep.join(mpath)
|
||||
pass
|
||||
|
||||
f = open(_spath)
|
||||
key = f.read()
|
||||
f.close()
|
||||
|
||||
_port = port if port else _config['system']['app']['port']
|
||||
url = f"http://localhost:{_port}/reload"
|
||||
resp = requests.post(url, headers={"key":key})
|
||||
if resp.status_code == 200 :
|
||||
_msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold] : successfully reloaded {url}"""
|
||||
else:
|
||||
_msg = f"""{FAILED} failed to reload, status code {resp.status_code}\n{url}
|
||||
"""
|
||||
else:
|
||||
_msg = f"""{FAILED} no secure key found in manifest to request reload"""
|
||||
print (_msg)
|
||||
|
||||
Loading…
Reference in new issue