mirror of http://localhost:9400/cloud/cms
commit
ead753cda0
@ -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