cmdbox 0.5.3.1__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cmdbox might be problematic. Click here for more details.
- cmdbox/app/auth/__init__.py +0 -0
- cmdbox/app/auth/azure_signin.py +38 -0
- cmdbox/app/auth/azure_signin_saml.py +12 -0
- cmdbox/app/auth/github_signin.py +38 -0
- cmdbox/app/auth/google_signin.py +32 -0
- cmdbox/app/auth/signin.py +490 -287
- cmdbox/app/auth/signin_saml.py +61 -0
- cmdbox/app/common.py +48 -3
- cmdbox/app/edge.py +182 -213
- cmdbox/app/edge_tool.py +177 -0
- cmdbox/app/feature.py +10 -10
- cmdbox/app/features/cli/agent_base.py +477 -0
- cmdbox/app/features/cli/audit_base.py +1 -1
- cmdbox/app/features/cli/cmdbox_audit_search.py +24 -1
- cmdbox/app/features/cli/cmdbox_client_file_download.py +1 -1
- cmdbox/app/features/cli/cmdbox_cmd_list.py +105 -0
- cmdbox/app/features/cli/cmdbox_cmd_load.py +104 -0
- cmdbox/app/features/cli/cmdbox_edge_config.py +21 -7
- cmdbox/app/features/cli/cmdbox_edge_start.py +1 -1
- cmdbox/app/features/cli/cmdbox_gui_start.py +9 -132
- cmdbox/app/features/cli/cmdbox_gui_stop.py +4 -21
- cmdbox/app/features/cli/cmdbox_server_start.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_apikey_add.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_apikey_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_genpass.py +0 -3
- cmdbox/app/features/cli/cmdbox_web_group_add.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_edit.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_list.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_start.py +119 -104
- cmdbox/app/features/cli/cmdbox_web_stop.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_add.py +4 -4
- cmdbox/app/features/cli/cmdbox_web_user_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_edit.py +4 -4
- cmdbox/app/features/cli/cmdbox_web_user_list.py +1 -1
- cmdbox/app/features/web/cmdbox_web_agent.py +250 -0
- cmdbox/app/features/web/cmdbox_web_do_signin.py +79 -103
- cmdbox/app/features/web/cmdbox_web_exec_cmd.py +8 -3
- cmdbox/app/features/web/cmdbox_web_signin.py +26 -4
- cmdbox/app/features/web/cmdbox_web_users.py +2 -0
- cmdbox/app/options.py +55 -2
- cmdbox/app/web.py +155 -27
- cmdbox/extensions/features.yml +18 -0
- cmdbox/extensions/sample_project/sample/app/features/cli/__init__.py +0 -0
- cmdbox/extensions/sample_project/sample/app/features/web/__init__.py +0 -0
- cmdbox/extensions/sample_project/sample/extensions/features.yml +23 -0
- cmdbox/extensions/sample_project/sample/extensions/user_list.yml +40 -6
- cmdbox/extensions/user_list.yml +37 -6
- cmdbox/licenses/{LICENSE.starlette.0.41.3(BSD License).txt → LICENSE.Authlib.1.5.2(BSD License).txt } +3 -1
- cmdbox/licenses/{LICENSE.pydantic_core.2.33.0(MIT License).txt → LICENSE.Deprecated.1.2.18(MIT License).txt } +2 -2
- cmdbox/licenses/{LICENSE.more-itertools.10.6.0(MIT License).txt → LICENSE.SQLAlchemy.2.0.40(MIT License).txt } +1 -1
- cmdbox/licenses/LICENSE.aiohttp.3.11.18(Apache Software License).txt +13 -0
- cmdbox/licenses/LICENSE.aiosignal.1.3.2(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.async-timeout.5.0.1(Apache Software License).txt +13 -0
- cmdbox/licenses/{LICENSE.watchfiles.1.0.0(MIT License).txt → LICENSE.attrs.25.3.0(UNKNOWN).txt} +1 -1
- cmdbox/licenses/{LICENSE.anyio.4.6.2.post1(MIT License).txt → LICENSE.cachetools.5.5.2(MIT License).txt } +1 -1
- cmdbox/licenses/LICENSE.distro.1.9.0(Apache Software License).txt +202 -0
- cmdbox/licenses/{LICENSE.pydantic_core.2.33.1(MIT License).txt → LICENSE.docstring_parser.0.16(MIT License).txt } +1 -1
- cmdbox/licenses/LICENSE.filelock.3.18.0(The Unlicense (Unlicense)).txt +24 -0
- cmdbox/licenses/LICENSE.frozenlist.1.6.0(Apache-2.0).txt +201 -0
- cmdbox/licenses/{LICENSE.starlette.0.46.1(BSD License).txt → LICENSE.fsspec.2025.3.2(BSD License).txt } +3 -1
- cmdbox/licenses/{LICENSE.argcomplete.3.6.1(Apache Software License).txt → LICENSE.google-adk.0.5.0(Apache Software License).txt } +25 -0
- cmdbox/licenses/LICENSE.google-api-python-client.2.169.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.google-auth-httplib2.0.2.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.google-auth.2.40.1(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.google-cloud-aiplatform.1.92.0(Apache 2.0).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-bigquery.3.31.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-core.2.4.3(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-resource-manager.1.14.2(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-secret-manager.2.23.3(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-speech.2.32.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-storage.2.19.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-trace.1.16.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-crc32c.1.7.1(Apache 2.0).txt +202 -0
- cmdbox/licenses/LICENSE.google-genai.1.14.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-resumable-media.2.7.2(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.googleapis-common-protos.1.70.0(Apache Software License).txt +202 -0
- cmdbox/licenses/{LICENSE.fastapi.0.115.5(MIT License).txt → LICENSE.graphviz.0.20.3(MIT License).txt } +1 -1
- cmdbox/licenses/LICENSE.grpc-google-iam-v1.0.14.2(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.grpcio-status.1.71.0(Apache Software License).txt +610 -0
- cmdbox/licenses/LICENSE.grpcio.1.71.0(Apache Software License).txt +610 -0
- cmdbox/licenses/{LICENSE.uvicorn.0.34.0(BSD License).txt → LICENSE.httpcore.1.0.9(BSD License).txt } +1 -1
- cmdbox/licenses/LICENSE.httplib2.0.22.0(MIT License).txt +23 -0
- cmdbox/licenses/{LICENSE.tomli.2.1.0(MIT License).txt → LICENSE.httpx-sse.0.4.0(MIT).txt} +1 -1
- cmdbox/licenses/LICENSE.httpx.0.28.1(BSD License).txt +12 -0
- cmdbox/licenses/LICENSE.huggingface-hub.0.31.1(Apache Software License).txt +201 -0
- cmdbox/licenses/{LICENSE.charset-normalizer.3.4.0(MIT License).txt → LICENSE.jsonschema-specifications.2025.4.1(UNKNOWN).txt} +5 -7
- cmdbox/licenses/LICENSE.jsonschema.4.23.0(MIT License).txt +19 -0
- cmdbox/licenses/{LICENSE.pkginfo.1.10.0(MIT License).txt → LICENSE.litellm.1.69.0(MIT License).txt } +6 -1
- cmdbox/licenses/{LICENSE.redis.5.2.1(MIT License).txt → LICENSE.mcp.1.8.0(MIT License).txt } +1 -1
- cmdbox/licenses/LICENSE.multidict.6.4.3(Apache Software License).txt +13 -0
- cmdbox/licenses/{LICENSE.argcomplete.3.5.1(Apache Software License).txt → LICENSE.openai.1.75.0(Apache Software License).txt } +25 -1
- cmdbox/licenses/LICENSE.opentelemetry-api.1.33.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-exporter-gcp-trace.1.9.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-resourcedetector-gcp.1.9.0a0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-sdk.1.33.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-semantic-conventions.0.54b0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.propcache.0.3.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.proto-plus.1.26.1(Apache Software License).txt +202 -0
- cmdbox/licenses/{LICENSE.Pygments.2.18.0(BSD License).txt → LICENSE.protobuf.5.29.4(3-Clause BSD License).txt } +15 -8
- cmdbox/licenses/LICENSE.pyasn1.0.6.1(BSD License).txt +24 -0
- cmdbox/licenses/LICENSE.pyasn1_modules.0.4.2(BSD License).txt +24 -0
- cmdbox/licenses/LICENSE.pydantic-settings.2.9.1(MIT License).txt +21 -0
- cmdbox/licenses/{LICENSE.gevent.25.4.1(MIT).txt → LICENSE.pyparsing.3.2.3(MIT License).txt } +5 -12
- cmdbox/licenses/LICENSE.python-dateutil.2.9.0.post0(Apache Software License; BSD License).txt +54 -0
- cmdbox/licenses/LICENSE.referencing.0.36.2(UNKNOWN).txt +19 -0
- cmdbox/licenses/LICENSE.regex.2024.11.6(Apache Software License).txt +208 -0
- cmdbox/licenses/LICENSE.rpds-py.0.24.0(MIT).txt +19 -0
- cmdbox/licenses/{LICENSE.python-multipart.0.0.17(Apache Software License).txt → LICENSE.rsa.4.9.1(Apache Software License).txt } +1 -2
- cmdbox/licenses/{LICENSE.sphinx-intl.2.3.0(BSD License).txt → LICENSE.shapely.2.1.0(BSD License).txt } +6 -2
- cmdbox/licenses/LICENSE.sse-starlette.2.3.4(BSD License).txt +27 -0
- cmdbox/licenses/LICENSE.tiktoken.0.9.0(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.tokenizers.0.21.1(Apache Software License).txt +1 -0
- cmdbox/licenses/{LICENSE.six.1.16.0(MIT License).txt → LICENSE.tqdm.4.67.1(MIT License; Mozilla Public License 2.0 (MPL 2.0)).txt } +32 -1
- cmdbox/licenses/{LICENSE.rich.13.9.4(MIT License).txt → LICENSE.tzlocal.5.3.1(MIT License).txt } +3 -3
- cmdbox/licenses/LICENSE.uritemplate.4.1.1(Apache Software License; BSD License).txt +3 -0
- cmdbox/licenses/LICENSE.wrapt.1.17.2(BSD License).txt +24 -0
- cmdbox/licenses/LICENSE.yarl.1.20.0(Apache Software License).txt +202 -0
- cmdbox/licenses/files.txt +111 -17
- cmdbox/logconf_agent.yml +38 -0
- cmdbox/logconf_audit.yml +13 -5
- cmdbox/logconf_client.yml +13 -5
- cmdbox/logconf_cmdbox.yml +13 -5
- cmdbox/logconf_edge.yml +13 -5
- cmdbox/logconf_gui.yml +13 -5
- cmdbox/logconf_server.yml +13 -5
- cmdbox/logconf_web.yml +13 -5
- cmdbox/version.py +3 -2
- cmdbox/web/agent.html +263 -0
- cmdbox/web/assets/cmdbox/agent.js +335 -0
- cmdbox/web/assets/cmdbox/common.js +1111 -1020
- cmdbox/web/assets/cmdbox/signin.js +16 -3
- cmdbox/web/assets/cmdbox/users.js +1 -1
- cmdbox/web/assets/filer/filer.js +4 -2
- cmdbox/web/signin.html +10 -6
- {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/METADATA +132 -35
- {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/RECORD +161 -123
- cmdbox/app/features/web/cmdbox_web_load_pin.py +0 -43
- cmdbox/app/features/web/cmdbox_web_save_pin.py +0 -42
- cmdbox/licenses/LICENSE.Jinja2.3.1.4(BSD License).txt +0 -28
- cmdbox/licenses/LICENSE.Sphinx.8.1.3(BSD License).txt +0 -31
- cmdbox/licenses/LICENSE.babel.2.16.0(BSD License).txt +0 -27
- cmdbox/licenses/LICENSE.certifi.2025.1.31(Mozilla Public License 2.0 (MPL 2.0)).txt +0 -20
- cmdbox/licenses/LICENSE.click.8.1.8(BSD License).txt +0 -28
- cmdbox/licenses/LICENSE.cryptography.44.0.2(Apache Software License; BSD License).txt +0 -3
- cmdbox/licenses/LICENSE.greenlet.3.2.0(MIT AND Python-2.0).txt +0 -30
- cmdbox/licenses/LICENSE.keyring.25.5.0(MIT License).txt +0 -17
- cmdbox/licenses/LICENSE.numpy.2.2.4(BSD License).txt +0 -950
- cmdbox/licenses/LICENSE.pillow.11.0.0(CMU License (MIT-CMU)).txt +0 -1226
- cmdbox/licenses/LICENSE.pillow.11.1.0(CMU License (MIT-CMU)).txt +0 -1213
- cmdbox/licenses/LICENSE.prettytable.3.12.0(BSD License).txt +0 -30
- cmdbox/licenses/LICENSE.prompt_toolkit.3.0.50(BSD License).txt +0 -27
- cmdbox/licenses/LICENSE.psycopg.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +0 -165
- cmdbox/licenses/LICENSE.pydantic.2.11.1(MIT License).txt +0 -21
- cmdbox/licenses/LICENSE.pydantic.2.11.3(MIT License).txt +0 -21
- cmdbox/licenses/LICENSE.python-dotenv.1.0.1(BSD License).txt +0 -27
- cmdbox/licenses/LICENSE.twine.5.1.1(Apache Software License).txt +0 -174
- cmdbox/licenses/LICENSE.typing_extensions.4.13.0(UNKNOWN).txt +0 -279
- cmdbox/licenses/LICENSE.urllib3.2.2.3(MIT License).txt +0 -21
- cmdbox/licenses/LICENSE.urllib3.2.3.0(MIT License).txt +0 -21
- cmdbox/licenses/LICENSE.uvicorn.0.34.1(BSD License).txt +0 -27
- cmdbox/licenses/LICENSE.watchfiles.1.0.4(MIT License).txt +0 -21
- cmdbox/licenses/LICENSE.websockets.14.1(BSD License).txt +0 -24
- cmdbox/licenses/LICENSE.zope.interface.7.1.1(Zope Public License).txt +0 -44
- /cmdbox/licenses/{LICENSE.typing_extensions.4.12.2(Python Software Foundation License).txt → LICENSE.aiohappyeyeballs.2.6.1(Python Software Foundation License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.certifi.2024.8.30(Mozilla Public License 2.0 (MPL 2.0)).txt → LICENSE.certifi.2025.4.26(Mozilla Public License 2.0 (MPL 2.0)).txt} +0 -0
- /cmdbox/licenses/{LICENSE.charset-normalizer.3.4.1(MIT License).txt → LICENSE.charset-normalizer.3.4.2(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.click.8.1.7(BSD License).txt → LICENSE.click.8.2.0(UNKNOWN).txt} +0 -0
- /cmdbox/licenses/{LICENSE.cryptography.43.0.3(Apache Software License; BSD License).txt → LICENSE.cryptography.44.0.3(Apache Software License; BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.gevent.24.11.1(MIT License).txt → LICENSE.gevent.25.4.2(MIT).txt} +0 -0
- /cmdbox/licenses/{LICENSE.importlib_metadata.8.5.0(Apache Software License).txt → LICENSE.google-api-core.2.24.2(Apache Software License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.greenlet.3.1.1(MIT License).txt → LICENSE.greenlet.3.2.2(MIT AND Python-2.0).txt} +0 -0
- /cmdbox/licenses/{LICENSE.h11.0.14.0(MIT License).txt → LICENSE.h11.0.16.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.nh3.0.2.18(MIT).txt → LICENSE.jiter.0.9.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.more-itertools.10.5.0(MIT License).txt → LICENSE.more-itertools.10.7.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.numpy.2.1.3(BSD License).txt → LICENSE.numpy.2.2.5(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.packaging.24.2(Apache Software License; BSD License).txt → LICENSE.packaging.25.0(Apache Software License; BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.psycopg-binary.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt → LICENSE.psycopg-binary.3.2.7(GNU Lesser General Public License v3 (LGPLv3)).txt} +0 -0
- /cmdbox/licenses/{LICENSE.psycopg-pool.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt → LICENSE.psycopg.3.2.7(GNU Lesser General Public License v3 (LGPLv3)).txt} +0 -0
- /cmdbox/licenses/{LICENSE.pydantic.2.10.2(MIT License).txt → LICENSE.pydantic.2.11.4(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.pydantic_core.2.27.1(MIT License).txt → LICENSE.pydantic_core.2.33.2(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.redis.5.2.0(MIT License).txt → LICENSE.redis.6.0.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.snowballstemmer.2.2.0(BSD License).txt → LICENSE.snowballstemmer.3.0.1(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.uvicorn.0.32.1(BSD License).txt → LICENSE.uvicorn.0.34.2(BSD License).txt} +0 -0
- {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/LICENSE +0 -0
- {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/WHEEL +0 -0
- {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/entry_points.txt +0 -0
- {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from cmdbox.app import common, feature
|
|
2
|
+
from cmdbox.app.auth import signin
|
|
3
|
+
from cmdbox.app.commons import convert
|
|
4
|
+
from cmdbox.app.web import Web
|
|
5
|
+
from fastapi import FastAPI, Depends, HTTPException, Request, Response, WebSocket
|
|
6
|
+
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
7
|
+
from starlette.websockets import WebSocketDisconnect
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from starlette.applications import Starlette
|
|
10
|
+
from starlette.datastructures import UploadFile
|
|
11
|
+
from starlette.routing import Mount
|
|
12
|
+
from typing import Dict, Any, Tuple, List, Union
|
|
13
|
+
import locale
|
|
14
|
+
import logging
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
import traceback
|
|
18
|
+
|
|
19
|
+
class Agent(feature.WebFeature):
|
|
20
|
+
def route(self, web:Web, app:FastAPI) -> None:
|
|
21
|
+
"""
|
|
22
|
+
webモードのルーティングを設定します
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
web (Web): Webオブジェクト
|
|
26
|
+
app (FastAPI): FastAPIオブジェクト
|
|
27
|
+
"""
|
|
28
|
+
if web.agent_html is not None:
|
|
29
|
+
if not web.agent_html.is_file():
|
|
30
|
+
raise FileNotFoundError(f'agent_html is not found. ({web.agent_html})')
|
|
31
|
+
with open(web.agent_html, 'r', encoding='utf-8') as f:
|
|
32
|
+
web.agent_html_data = f.read()
|
|
33
|
+
|
|
34
|
+
@app.get('/agent', response_class=HTMLResponse)
|
|
35
|
+
@app.post('/agent', response_class=HTMLResponse)
|
|
36
|
+
async def agent(req:Request, res:Response):
|
|
37
|
+
signin = web.signin.check_signin(req, res)
|
|
38
|
+
if signin is not None:
|
|
39
|
+
return signin
|
|
40
|
+
res.headers['Access-Control-Allow-Origin'] = '*'
|
|
41
|
+
web.options.audit_exec(req, res, web)
|
|
42
|
+
return web.agent_html_data
|
|
43
|
+
|
|
44
|
+
@app.post('/agent/session/list')
|
|
45
|
+
async def agent_session_list(req:Request, res:Response):
|
|
46
|
+
signin = web.signin.check_signin(req, res)
|
|
47
|
+
if signin is not None:
|
|
48
|
+
return signin
|
|
49
|
+
if web.agent_runner is None:
|
|
50
|
+
web.logger.error(f"agent_runner is null. Start web mode with `--agent use`.")
|
|
51
|
+
raise HTTPException(status_code=500, detail='agent_runner is null. Start web mode with `--agent use`.')
|
|
52
|
+
res.headers['Access-Control-Allow-Origin'] = '*'
|
|
53
|
+
web.options.audit_exec(req, res, web)
|
|
54
|
+
# ユーザー名を取得する
|
|
55
|
+
user_id = common.random_string(16)
|
|
56
|
+
if 'signin' in req.session:
|
|
57
|
+
user_id = req.session['signin']['name']
|
|
58
|
+
form = await req.form()
|
|
59
|
+
session_id = form.get('session_id', None)
|
|
60
|
+
sessions = await web.list_agent_sessions(web.agent_runner.session_service, user_id, session_id=session_id)
|
|
61
|
+
data = [dict(id=s.id, app_name=s.app_name, user_id=s.user_id, last_update_time=s.last_update_time,
|
|
62
|
+
events=[dict(author=ev.author,text=ev.content.parts[0].text) for ev in s.events if ev.content and ev.content.parts]) for s in sessions]
|
|
63
|
+
data.reverse() # 最新のセッションを先頭にする
|
|
64
|
+
return dict(success=data)
|
|
65
|
+
|
|
66
|
+
@app.post('/agent/session/delete')
|
|
67
|
+
async def agent_session_delete(req:Request, res:Response):
|
|
68
|
+
signin = web.signin.check_signin(req, res)
|
|
69
|
+
if signin is not None:
|
|
70
|
+
return signin
|
|
71
|
+
if web.agent_runner is None:
|
|
72
|
+
web.logger.error(f"agent_runner is null. Start web mode with `--agent use`.")
|
|
73
|
+
raise HTTPException(status_code=500, detail='agent_runner is null. Start web mode with `--agent use`.')
|
|
74
|
+
res.headers['Access-Control-Allow-Origin'] = '*'
|
|
75
|
+
web.options.audit_exec(req, res, web)
|
|
76
|
+
# ユーザー名を取得する
|
|
77
|
+
user_id = common.random_string(16)
|
|
78
|
+
if 'signin' in req.session:
|
|
79
|
+
user_id = req.session['signin']['name']
|
|
80
|
+
form = await req.form()
|
|
81
|
+
session_id = form.get('session_id', None)
|
|
82
|
+
await web.delete_agent_session(web.agent_runner.session_service, user_id, session_id=session_id)
|
|
83
|
+
return dict(success=True)
|
|
84
|
+
|
|
85
|
+
@app.websocket('/agent/chat/ws')
|
|
86
|
+
@app.websocket('/agent/chat/ws/{session_id}')
|
|
87
|
+
async def ws_chat(session_id:str=None, websocket:WebSocket=None, res:Response=None, scope=Depends(signin.create_request_scope)):
|
|
88
|
+
if not websocket:
|
|
89
|
+
raise HTTPException(status_code=400, detail='Expected WebSocket request.')
|
|
90
|
+
signin = web.signin.check_signin(websocket, res)
|
|
91
|
+
if signin is not None:
|
|
92
|
+
return signin
|
|
93
|
+
if web.agent_runner is None:
|
|
94
|
+
web.logger.error(f"agent_runner is null. Start web mode with `--agent use`.")
|
|
95
|
+
raise HTTPException(status_code=500, detail='agent_runner is null. Start web mode with `--agent use`.')
|
|
96
|
+
|
|
97
|
+
# これを行わねば非同期処理にならない。。
|
|
98
|
+
await websocket.accept()
|
|
99
|
+
# チャット処理
|
|
100
|
+
async for res in _chat(websocket.session, session_id, websocket, websocket.receive_text):
|
|
101
|
+
await websocket.send_text(res)
|
|
102
|
+
return dict(success="connected")
|
|
103
|
+
|
|
104
|
+
@app.api_route('/agent/chat/stream', methods=['GET', 'POST'])
|
|
105
|
+
@app.api_route('/agent/chat/stream/{session_id}', methods=['GET', 'POST'])
|
|
106
|
+
async def sse_chat(session_id:str=None, req:Request=None, res:Response=None):
|
|
107
|
+
signin = web.signin.check_signin(req, res)
|
|
108
|
+
if signin is not None:
|
|
109
|
+
return signin
|
|
110
|
+
def _marge_opt(opt, param):
|
|
111
|
+
for k in opt.keys():
|
|
112
|
+
if k in param: opt[k] = param[k]
|
|
113
|
+
return opt
|
|
114
|
+
content_type = req.headers.get('content-type')
|
|
115
|
+
opt = None
|
|
116
|
+
if content_type is None:
|
|
117
|
+
opt = req.query_params._dict
|
|
118
|
+
elif content_type.startswith('multipart/form-data'):
|
|
119
|
+
form = await req.form()
|
|
120
|
+
opt = dict()
|
|
121
|
+
for key, fv in form.multi_items():
|
|
122
|
+
if not isinstance(fv, UploadFile): continue
|
|
123
|
+
opt[key] = fv.file
|
|
124
|
+
elif content_type.startswith('application/json'):
|
|
125
|
+
opt = await req.json()
|
|
126
|
+
elif content_type.startswith('application/octet-stream'):
|
|
127
|
+
opt = json.loads(await req.body())
|
|
128
|
+
if opt is None:
|
|
129
|
+
raise HTTPException(status_code=400, detail='Expected JSON or form data.')
|
|
130
|
+
if opt['query'] is None or opt['query'] == '':
|
|
131
|
+
raise HTTPException(status_code=400, detail='Expected query.')
|
|
132
|
+
if web.agent_runner is None:
|
|
133
|
+
web.logger.error(f"agent_runner is null. Start web mode with `--agent use`.")
|
|
134
|
+
raise HTTPException(status_code=500, detail='agent_runner is null. Start web mode with `--agent use`.')
|
|
135
|
+
async def receive_text():
|
|
136
|
+
# 受信したデータを返す
|
|
137
|
+
if 'query' in opt:
|
|
138
|
+
query = opt['query']
|
|
139
|
+
del opt['query']
|
|
140
|
+
return query
|
|
141
|
+
raise self.SSEDisconnect('SSE disconnect')
|
|
142
|
+
# チャット処理
|
|
143
|
+
return StreamingResponse(
|
|
144
|
+
_chat(req.session, session_id, req, receive_text=receive_text)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async def _chat(session:Dict[str, Any], session_id:str, sock, receive_text=None):
|
|
148
|
+
if web.logger.level == logging.DEBUG:
|
|
149
|
+
web.logger.debug(f"agent_chat: connected")
|
|
150
|
+
# ユーザー名を取得する
|
|
151
|
+
user_id = common.random_string(16)
|
|
152
|
+
groups = []
|
|
153
|
+
if 'signin' in session:
|
|
154
|
+
user_id = session['signin']['name']
|
|
155
|
+
groups = session['signin']['groups']
|
|
156
|
+
# 言語認識
|
|
157
|
+
language, _ = locale.getlocale()
|
|
158
|
+
is_japan = language.find('Japan') >= 0 or language.find('ja_JP') >= 0
|
|
159
|
+
# セッションを作成する
|
|
160
|
+
agent_session = await web.create_agent_session(web.agent_runner.session_service, user_id, session_id=session_id)
|
|
161
|
+
startmsg = "こんにちは!何かお手伝いできることはありますか?" if is_japan else "Hello! Is there anything I can help you with?"
|
|
162
|
+
yield json.dumps(dict(message=startmsg), default=common.default_json_enc)
|
|
163
|
+
from google.genai import types
|
|
164
|
+
while True:
|
|
165
|
+
outputs = None
|
|
166
|
+
try:
|
|
167
|
+
query = await receive_text()
|
|
168
|
+
if query is None or query == '' or query == 'ping':
|
|
169
|
+
time.sleep(0.5)
|
|
170
|
+
continue
|
|
171
|
+
"""
|
|
172
|
+
if is_japan:
|
|
173
|
+
query += f"<important>なお現在のユーザーは'{user_id}'でgroupsは'{groups}'ですので引数に必要な時は指定してください。" + \
|
|
174
|
+
f"またsignin_fileの引数が必要な時は'{web.signin.signin_file}'を指定してください。</important>"
|
|
175
|
+
#f"またコマンド実行に必要なパラメータを確認し、以下の引数が必要な場合はこの値を使用してください。\n" + \
|
|
176
|
+
#f" host = {web.redis_host if web.redis_host else self.default_host}\n" + \
|
|
177
|
+
#f", port = {web.redis_port if web.redis_port else self.default_port}\n" + \
|
|
178
|
+
#f", password = {web.redis_password if web.redis_password else self.default_pass}\n" + \
|
|
179
|
+
#f", svname = {web.svname if web.svname else self.default_svname}\n"
|
|
180
|
+
else:
|
|
181
|
+
query += f"<important>The current user is '{user_id}' and the groups is '{groups}', so please specify it when necessary." + \
|
|
182
|
+
f"Also, if the signin_file argument is required for command execution, please specify '{web.signin.signin_file}'.</important>"
|
|
183
|
+
#f"Also check the parameters required to execute the command and use these values if the following arguments are required.\n" + \
|
|
184
|
+
#f" host = {web.redis_host if web.redis_host else self.default_host}\n" + \
|
|
185
|
+
#f", port = {web.redis_port if web.redis_port else self.default_port}\n" + \
|
|
186
|
+
#f", password = {web.redis_password if web.redis_password else self.default_pass}\n" + \
|
|
187
|
+
#f", svname = {web.svname if web.svname else self.default_svname}\n"
|
|
188
|
+
"""
|
|
189
|
+
web.options.audit_exec(sock, web, body=dict(agent_session=agent_session.id, user_id=user_id, groups=groups, query=query))
|
|
190
|
+
content = types.Content(role='user', parts=[types.Part(text=query)])
|
|
191
|
+
|
|
192
|
+
async for event in web.agent_runner.run_async(user_id=user_id, session_id=agent_session.id, new_message=content):
|
|
193
|
+
#web.agent_runner.session_service.append_event(agent_session, event)
|
|
194
|
+
outputs = dict()
|
|
195
|
+
if event.turn_complete:
|
|
196
|
+
outputs['turn_complete'] = True
|
|
197
|
+
yield json.dumps(outputs, default=common.default_json_enc)
|
|
198
|
+
if event.interrupted:
|
|
199
|
+
outputs['interrupted'] = True
|
|
200
|
+
yield json.dumps(outputs, default=common.default_json_enc)
|
|
201
|
+
#if event.is_final_response():
|
|
202
|
+
msg = None
|
|
203
|
+
if event.content and event.content.parts:
|
|
204
|
+
msg = "\n".join([p.text for p in event.content.parts if p and p.text])
|
|
205
|
+
elif event.actions and event.actions.escalate:
|
|
206
|
+
msg = f"Agent escalated: {event.error_message or 'No specific message.'}"
|
|
207
|
+
if msg:
|
|
208
|
+
outputs['message'] = msg
|
|
209
|
+
web.options.audit_exec(sock, web, body=dict(agent_session=agent_session.id, result=msg))
|
|
210
|
+
yield json.dumps(outputs, default=common.default_json_enc)
|
|
211
|
+
if event.is_final_response():
|
|
212
|
+
break
|
|
213
|
+
except WebSocketDisconnect:
|
|
214
|
+
web.logger.warning('chat: websocket disconnected.')
|
|
215
|
+
break
|
|
216
|
+
except self.SSEDisconnect as e:
|
|
217
|
+
break
|
|
218
|
+
except Exception as e:
|
|
219
|
+
web.logger.warning(f'chat error.', exc_info=True)
|
|
220
|
+
yield json.dumps(dict(message=f'<pre>{traceback.format_exc()}</pre>'), default=common.default_json_enc)
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
def toolmenu(self, web:Web) -> Dict[str, Any]:
|
|
224
|
+
"""
|
|
225
|
+
ツールメニューの情報を返します
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
web (Web): Webオブジェクト
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dict[str, Any]: ツールメニュー情報
|
|
232
|
+
|
|
233
|
+
Sample:
|
|
234
|
+
{
|
|
235
|
+
'filer': {
|
|
236
|
+
'html': 'Filer',
|
|
237
|
+
'href': 'filer',
|
|
238
|
+
'target': '_blank',
|
|
239
|
+
'css_class': 'dropdown-item'
|
|
240
|
+
'onclick': 'alert("filer")'
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
"""
|
|
244
|
+
return dict(agent=dict(html='Agent', href='agent', target='_blank', css_class='dropdown-item'))
|
|
245
|
+
|
|
246
|
+
class SSEDisconnect(Exception):
|
|
247
|
+
"""
|
|
248
|
+
SSEの切断を示す例外クラス
|
|
249
|
+
"""
|
|
250
|
+
pass
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
import urllib.parse
|
|
1
2
|
from cmdbox.app import common
|
|
2
|
-
from cmdbox.app.auth
|
|
3
|
+
from cmdbox.app.auth import signin, signin_saml, azure_signin, azure_signin_saml, github_signin, google_signin
|
|
3
4
|
from cmdbox.app.commons import convert
|
|
4
5
|
from cmdbox.app.features.web import cmdbox_web_signin
|
|
5
6
|
from cmdbox.app.web import Web
|
|
6
7
|
from fastapi import FastAPI, Request, Response, HTTPException
|
|
7
8
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
9
|
+
from typing import Any, Dict
|
|
8
10
|
import copy
|
|
9
11
|
import datetime
|
|
10
12
|
import importlib
|
|
11
13
|
import inspect
|
|
12
14
|
import json
|
|
13
15
|
import logging
|
|
14
|
-
import
|
|
15
|
-
import urllib.parse
|
|
16
|
+
import urllib
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class DoSignin(cmdbox_web_signin.Signin):
|
|
@@ -60,7 +61,7 @@ class DoSignin(cmdbox_web_signin.Signin):
|
|
|
60
61
|
if name == '' or passwd == '':
|
|
61
62
|
web.options.audit_exec(req, res, web, body=dict(msg='signin failed.'), audit_type='auth')
|
|
62
63
|
return RedirectResponse(url=f'/signin/{next}?error=1')
|
|
63
|
-
user = [u for u in signin_data['users'] if u['name'] == name and u['hash'] != 'oauth2']
|
|
64
|
+
user = [u for u in signin_data['users'] if u['name'] == name and u['hash'] != 'oauth2' and u['hash'] != 'saml']
|
|
64
65
|
if len(user) <= 0:
|
|
65
66
|
web.options.audit_exec(req, res, web, body=dict(msg='signin failed.'), audit_type='auth')
|
|
66
67
|
return RedirectResponse(url=f'/signin/{next}?error=1')
|
|
@@ -108,7 +109,7 @@ class DoSignin(cmdbox_web_signin.Signin):
|
|
|
108
109
|
last_update = web.user_data(None, uid, name, 'password', 'last_update')
|
|
109
110
|
notify_passchange = True if last_update is None else False
|
|
110
111
|
# パスワード認証の場合はパスワード有効期限チェック
|
|
111
|
-
if user['hash']!='oauth2' and 'password' in signin_data and not notify_passchange:
|
|
112
|
+
if user['hash']!='oauth2' and user['hash']!='saml' and 'password' in signin_data and not notify_passchange:
|
|
112
113
|
last_update = datetime.datetime.strptime(last_update, '%Y-%m-%dT%H:%M:%S')
|
|
113
114
|
# パスワード有効期限
|
|
114
115
|
expiration = signin_data['password']['expiration']
|
|
@@ -152,7 +153,7 @@ class DoSignin(cmdbox_web_signin.Signin):
|
|
|
152
153
|
members = inspect.getmembers(mod, inspect.isclass)
|
|
153
154
|
signin_data = web.signin.get_data()
|
|
154
155
|
for name, cls in members:
|
|
155
|
-
if cls is Signin or issubclass(cls, Signin):
|
|
156
|
+
if cls is signin.Signin or issubclass(cls, signin.Signin):
|
|
156
157
|
sobj = cls(web.logger, web.signin_file, signin_data, appcls, ver)
|
|
157
158
|
return sobj
|
|
158
159
|
return None
|
|
@@ -161,9 +162,10 @@ class DoSignin(cmdbox_web_signin.Signin):
|
|
|
161
162
|
raise e
|
|
162
163
|
|
|
163
164
|
signin_data = web.signin.get_data()
|
|
164
|
-
self.google_signin =
|
|
165
|
-
self.github_signin =
|
|
166
|
-
self.azure_signin =
|
|
165
|
+
self.google_signin = google_signin.GoogleSignin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
|
|
166
|
+
self.github_signin = github_signin.GithubSignin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
|
|
167
|
+
self.azure_signin = azure_signin.AzureSignin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
|
|
168
|
+
self.azure_saml_signin = azure_signin_saml.AzyreSigninSAML(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
|
|
167
169
|
if signin_data is not None:
|
|
168
170
|
# signinオブジェクトの指定があった場合読込む
|
|
169
171
|
if 'signin_module' in signin_data['oauth2']['providers']['google']:
|
|
@@ -175,6 +177,9 @@ class DoSignin(cmdbox_web_signin.Signin):
|
|
|
175
177
|
if 'signin_module' in signin_data['oauth2']['providers']['azure']:
|
|
176
178
|
sobj = _load_signin(web, signin_data['oauth2']['providers']['azure']['signin_module'], self.appcls, self.ver)
|
|
177
179
|
self.azure_signin = sobj if sobj is not None else self.azure_signin
|
|
180
|
+
if 'signin_module' in signin_data['saml']['providers']['azure']:
|
|
181
|
+
sobj = _load_signin(web, signin_data['saml']['providers']['azure']['signin_module'], self.appcls, self.ver)
|
|
182
|
+
self.azure_saml_signin = sobj if sobj is not None else self.azure_saml_signin
|
|
178
183
|
|
|
179
184
|
def _set_session(req:Request, user:dict, email:str, hashed_password:str, access_token:str, group_names:list, gids:list):
|
|
180
185
|
"""
|
|
@@ -209,20 +214,10 @@ class DoSignin(cmdbox_web_signin.Signin):
|
|
|
209
214
|
@app.get('/oauth2/google/callback')
|
|
210
215
|
async def oauth2_google_callback(req:Request, res:Response):
|
|
211
216
|
conf = web.signin.get_data()['oauth2']['providers']['google']
|
|
212
|
-
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
213
217
|
next = req.query_params['state']
|
|
214
|
-
data = {'code': req.query_params['code'],
|
|
215
|
-
'client_id': conf['client_id'],
|
|
216
|
-
'client_secret': conf['client_secret'],
|
|
217
|
-
'redirect_uri': conf['redirect_uri'],
|
|
218
|
-
'grant_type': 'authorization_code'}
|
|
219
|
-
query = '&'.join([f'{k}={urllib.parse.quote(v)}' for k, v in data.items()])
|
|
220
218
|
try:
|
|
221
219
|
# アクセストークン取得
|
|
222
|
-
|
|
223
|
-
token_resp.raise_for_status()
|
|
224
|
-
token_json = token_resp.json()
|
|
225
|
-
access_token = token_json['access_token']
|
|
220
|
+
access_token = self.google_signin.request_access_token(conf, req, res)
|
|
226
221
|
return await oauth2_google_session(access_token, next, req, res)
|
|
227
222
|
except Exception as e:
|
|
228
223
|
web.logger.warning(f'Failed to get token. {e}', exc_info=True)
|
|
@@ -230,45 +225,15 @@ class DoSignin(cmdbox_web_signin.Signin):
|
|
|
230
225
|
|
|
231
226
|
@app.get('/oauth2/google/session/{access_token}/{next}')
|
|
232
227
|
async def oauth2_google_session(access_token:str, next:str, req:Request, res:Response):
|
|
233
|
-
|
|
234
|
-
# ユーザー情報取得(email)
|
|
235
|
-
user_info_resp = requests.get(
|
|
236
|
-
url='https://www.googleapis.com/oauth2/v1/userinfo',
|
|
237
|
-
headers={'Authorization': f'Bearer {access_token}'}
|
|
238
|
-
)
|
|
239
|
-
user_info_resp.raise_for_status()
|
|
240
|
-
user_info_json = user_info_resp.json()
|
|
241
|
-
email = user_info_json['email']
|
|
242
|
-
# サインイン判定
|
|
243
|
-
jadge, user = self.google_signin.jadge(access_token, email)
|
|
244
|
-
if not jadge:
|
|
245
|
-
return RedirectResponse(url=f'/signin/{next}?error=appdeny')
|
|
246
|
-
# グループ取得
|
|
247
|
-
group_names, gids = self.google_signin.get_groups(access_token, user)
|
|
248
|
-
# セッションに保存
|
|
249
|
-
_set_session(req, user, email, None, access_token, group_names, gids)
|
|
250
|
-
return RedirectResponse(url=f'../../{next}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
|
|
251
|
-
except Exception as e:
|
|
252
|
-
web.logger.warning(f'Failed to get token. {e}', exc_info=True)
|
|
253
|
-
raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
|
|
228
|
+
return await oauth2_login_session(self.google_signin, access_token, next, req, res)
|
|
254
229
|
|
|
255
230
|
@app.get('/oauth2/github/callback')
|
|
256
231
|
async def oauth2_github_callback(req:Request, res:Response):
|
|
257
232
|
conf = web.signin.get_data()['oauth2']['providers']['github']
|
|
258
|
-
headers = {'Content-Type': 'application/x-www-form-urlencoded',
|
|
259
|
-
'Accept': 'application/json'}
|
|
260
233
|
next = req.query_params['state']
|
|
261
|
-
data = {'code': req.query_params['code'],
|
|
262
|
-
'client_id': conf['client_id'],
|
|
263
|
-
'client_secret': conf['client_secret'],
|
|
264
|
-
'redirect_uri': conf['redirect_uri']}
|
|
265
|
-
query = '&'.join([f'{k}={urllib.parse.quote(v)}' for k, v in data.items()])
|
|
266
234
|
try:
|
|
267
235
|
# アクセストークン取得
|
|
268
|
-
|
|
269
|
-
token_resp.raise_for_status()
|
|
270
|
-
token_json = token_resp.json()
|
|
271
|
-
access_token = token_json['access_token']
|
|
236
|
+
access_token = self.github_signin.request_access_token(conf, req, res)
|
|
272
237
|
return await oauth2_github_session(access_token, next, req, res)
|
|
273
238
|
except Exception as e:
|
|
274
239
|
web.logger.warning(f'Failed to get token. {e}', exc_info=True)
|
|
@@ -276,53 +241,15 @@ class DoSignin(cmdbox_web_signin.Signin):
|
|
|
276
241
|
|
|
277
242
|
@app.get('/oauth2/github/session/{access_token}/{next}')
|
|
278
243
|
async def oauth2_github_session(access_token:str, next:str, req:Request, res:Response):
|
|
279
|
-
|
|
280
|
-
# ユーザー情報取得(email)
|
|
281
|
-
user_info_resp = requests.get(
|
|
282
|
-
url='https://api.github.com/user/emails',
|
|
283
|
-
headers={'Authorization': f'Bearer {access_token}'}
|
|
284
|
-
)
|
|
285
|
-
user_info_resp.raise_for_status()
|
|
286
|
-
user_info_json = user_info_resp.json()
|
|
287
|
-
if type(user_info_json) == list:
|
|
288
|
-
email = 'notfound'
|
|
289
|
-
for u in user_info_json:
|
|
290
|
-
if u['primary']:
|
|
291
|
-
email = u['email']
|
|
292
|
-
break
|
|
293
|
-
# サインイン判定
|
|
294
|
-
jadge, user = self.github_signin.jadge(access_token, email)
|
|
295
|
-
if not jadge:
|
|
296
|
-
return RedirectResponse(url=f'/signin/{next}?error=appdeny')
|
|
297
|
-
# グループ取得
|
|
298
|
-
group_names, gids = self.github_signin.get_groups(access_token, user)
|
|
299
|
-
# セッションに保存
|
|
300
|
-
_set_session(req, user, email, None, access_token, group_names, gids)
|
|
301
|
-
return RedirectResponse(url=f'../../{next}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
|
|
302
|
-
except Exception as e:
|
|
303
|
-
web.logger.warning(f'Failed to get token. {e}', exc_info=True)
|
|
304
|
-
raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
|
|
244
|
+
return await oauth2_login_session(self.github_signin, access_token, next, req, res)
|
|
305
245
|
|
|
306
246
|
@app.get('/oauth2/azure/callback')
|
|
307
247
|
async def oauth2_azure_callback(req:Request, res:Response):
|
|
308
248
|
conf = web.signin.get_data()['oauth2']['providers']['azure']
|
|
309
|
-
headers = {'Content-Type': 'application/x-www-form-urlencoded',
|
|
310
|
-
'Accept': 'application/json'}
|
|
311
249
|
next = req.query_params['state']
|
|
312
|
-
data = {'tenant': conf['tenant_id'],
|
|
313
|
-
'code': req.query_params['code'],
|
|
314
|
-
'scope': " ".join(conf['scope']),
|
|
315
|
-
'client_id': conf['client_id'],
|
|
316
|
-
#'client_secret': conf['client_secret'],
|
|
317
|
-
'redirect_uri': conf['redirect_uri'],
|
|
318
|
-
'grant_type': 'authorization_code'}
|
|
319
|
-
query = '&'.join([f'{k}={urllib.parse.quote(v)}' for k, v in data.items()])
|
|
320
250
|
try:
|
|
321
251
|
# アクセストークン取得
|
|
322
|
-
|
|
323
|
-
token_resp.raise_for_status()
|
|
324
|
-
token_json = token_resp.json()
|
|
325
|
-
access_token = token_json['access_token']
|
|
252
|
+
access_token = self.azure_signin.request_access_token(conf, req, res)
|
|
326
253
|
return await oauth2_azure_session(access_token, next, req, res)
|
|
327
254
|
except Exception as e:
|
|
328
255
|
web.logger.warning(f'Failed to get token. {e}', exc_info=True)
|
|
@@ -330,26 +257,75 @@ class DoSignin(cmdbox_web_signin.Signin):
|
|
|
330
257
|
|
|
331
258
|
@app.get('/oauth2/azure/session/{access_token}/{next}')
|
|
332
259
|
async def oauth2_azure_session(access_token:str, next:str, req:Request, res:Response):
|
|
260
|
+
return await oauth2_login_session(self.azure_signin, access_token, next, req, res)
|
|
261
|
+
|
|
262
|
+
async def oauth2_login_session(signin:signin.Signin, access_token:str, next:str, req:Request, res:Response):
|
|
333
263
|
try:
|
|
334
264
|
# ユーザー情報取得(email)
|
|
335
|
-
|
|
336
|
-
url='https://graph.microsoft.com/v1.0/me',
|
|
337
|
-
#url='https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$Top=999',
|
|
338
|
-
headers={'Authorization': f'Bearer {access_token}'}
|
|
339
|
-
)
|
|
340
|
-
user_info_resp.raise_for_status()
|
|
341
|
-
user_info_json = user_info_resp.json()
|
|
342
|
-
if isinstance(user_info_json, dict):
|
|
343
|
-
email = user_info_json.get('mail', 'notfound')
|
|
265
|
+
email = signin.get_email(access_token)
|
|
344
266
|
# サインイン判定
|
|
345
|
-
jadge, user =
|
|
267
|
+
jadge, user = signin.jadge(email)
|
|
346
268
|
if not jadge:
|
|
347
269
|
return RedirectResponse(url=f'/signin/{next}?error=appdeny')
|
|
348
270
|
# グループ取得
|
|
349
|
-
group_names, gids =
|
|
271
|
+
group_names, gids = signin.get_groups(access_token, user)
|
|
350
272
|
# セッションに保存
|
|
351
273
|
_set_session(req, user, email, None, access_token, group_names, gids)
|
|
352
274
|
return RedirectResponse(url=f'../../{next}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
|
|
353
275
|
except Exception as e:
|
|
354
276
|
web.logger.warning(f'Failed to get token. {e}', exc_info=True)
|
|
355
277
|
raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
|
|
278
|
+
|
|
279
|
+
@app.post('/saml/azure/callback')
|
|
280
|
+
async def saml_azure_callback(req:Request, res:Response):
|
|
281
|
+
form = await req.form()
|
|
282
|
+
return await saml_login_callback('azure', self.azure_saml_signin, form, None, req, res)
|
|
283
|
+
|
|
284
|
+
@app.get('/saml/azure/session/{saml_token}/{next}')
|
|
285
|
+
async def saml_azure_session(saml_token:str, next:str, req:Request, res:Response):
|
|
286
|
+
form = json.loads(convert.b64str2str(saml_token))
|
|
287
|
+
return await saml_login_callback('azure', self.azure_saml_signin, form, next, req, res)
|
|
288
|
+
|
|
289
|
+
async def saml_login_callback(prov, saml_signin:signin_saml.SigninSAML, form:Dict[str, Any], next:str, req:Request, res:Response):
|
|
290
|
+
"""
|
|
291
|
+
SAML認証のコールバック処理を行います
|
|
292
|
+
Args:
|
|
293
|
+
prov (str): SAMLプロバイダ名
|
|
294
|
+
saml_signin (signin_saml.SigninSAML): SAMLサインインオブジェクト
|
|
295
|
+
form (Dict[str, Any]): フォームデータ
|
|
296
|
+
req (Request): Requestオブジェクト
|
|
297
|
+
res (Response): Responseオブジェクト
|
|
298
|
+
"""
|
|
299
|
+
relay = form.get('RelayState')
|
|
300
|
+
query = urllib.parse.urlparse(relay).query if relay is not None else None
|
|
301
|
+
if next is None:
|
|
302
|
+
next = urllib.parse.parse_qs(query).get('next', None) if query is not None else None
|
|
303
|
+
next = next[0] if next is not None and len(next) > 0 else None
|
|
304
|
+
auth = await saml_signin.make_saml(prov, next, form, req, res)
|
|
305
|
+
auth.process_response() # Process IdP response
|
|
306
|
+
errors = auth.get_errors() # This method receives an array with the errors
|
|
307
|
+
if len(errors) == 0:
|
|
308
|
+
if not auth.is_authenticated(): # This check if the response was ok and the user data retrieved or not (user authenticated)
|
|
309
|
+
return RedirectResponse(url=f'/signin/{next}?error=saml_not_auth')
|
|
310
|
+
else:
|
|
311
|
+
# ユーザー情報取得
|
|
312
|
+
email = saml_signin.get_email(auth)
|
|
313
|
+
# サインイン判定
|
|
314
|
+
jadge, user = saml_signin.jadge(email)
|
|
315
|
+
if not jadge:
|
|
316
|
+
return RedirectResponse(url=f'/signin/{next}?error=appdeny')
|
|
317
|
+
# グループ取得
|
|
318
|
+
group_names, gids = saml_signin.get_groups(None, user)
|
|
319
|
+
# セッションに保存
|
|
320
|
+
_set_session(req, user, email, None, None, group_names, gids)
|
|
321
|
+
# SAML場合、ブラウザ制限によりリダイレクトでセッションクッキーが消えるので、HTMLで移動する
|
|
322
|
+
html = """
|
|
323
|
+
<html><head><meta http-equiv="refresh" content="0;url=../../{next}"></head>
|
|
324
|
+
<body style="background-color:#212529;color:#fff;">loading..</body>
|
|
325
|
+
<script type="text/javascript">window.location.href="../../{next}";</script></html>
|
|
326
|
+
""".format(next=next)
|
|
327
|
+
return HTMLResponse(content=html, headers=dict(signin="success"))
|
|
328
|
+
else:
|
|
329
|
+
msg = f"Error when processing SAML Response: {', '.join(errors)} {auth.get_last_error_reason()}"
|
|
330
|
+
web.logger.warning(msg)
|
|
331
|
+
raise HTTPException(status_code=500, detail=msg)
|
|
@@ -4,6 +4,7 @@ from cmdbox.app.features.cli import cmdbox_audit_search, cmdbox_audit_write
|
|
|
4
4
|
from cmdbox.app.features.web import cmdbox_web_load_cmd
|
|
5
5
|
from cmdbox.app.web import Web
|
|
6
6
|
from fastapi import FastAPI, Request, Response, HTTPException
|
|
7
|
+
from fastapi.responses import PlainTextResponse
|
|
7
8
|
from starlette.datastructures import UploadFile
|
|
8
9
|
from typing import Dict, Any, List
|
|
9
10
|
import html
|
|
@@ -178,8 +179,10 @@ class ExecCmd(cmdbox_web_load_cmd.LoadCmd):
|
|
|
178
179
|
sys.stdout = captured_output = io.StringIO()
|
|
179
180
|
ret_main = {}
|
|
180
181
|
logsize = 1024
|
|
182
|
+
console = common.create_console(file=old_stdout)
|
|
183
|
+
|
|
181
184
|
try:
|
|
182
|
-
|
|
185
|
+
console.log(f'EXEC: {opt_list}\n'[:logsize])
|
|
183
186
|
status, ret_main, obj = cmdbox_app.main(args_list=[common.chopdq(o) for o in opt_list], file_dict=file_dict, webcall=True)
|
|
184
187
|
if isinstance(obj, server.Server):
|
|
185
188
|
cmdbox_app.sv = obj
|
|
@@ -206,11 +209,11 @@ class ExecCmd(cmdbox_web_load_cmd.LoadCmd):
|
|
|
206
209
|
output = [dict(warn=f'The captured stdout was discarded because its size was larger than {capture_maxsize} bytes.')]
|
|
207
210
|
else:
|
|
208
211
|
output = [dict(warn='capture_stdout is off.')]
|
|
209
|
-
|
|
212
|
+
console.log(f'EXEC: {output}'[:logsize])
|
|
210
213
|
except Exception as e:
|
|
211
214
|
web.logger.disabled = False # ログ出力を有効にする
|
|
212
215
|
msg = f'exec_cmd error. {traceback.format_exc()}'
|
|
213
|
-
|
|
216
|
+
console.log(f'EXEC: {msg}'[:logsize])
|
|
214
217
|
web.logger.warning(msg)
|
|
215
218
|
output = [dict(warn=f'<pre>{html.escape(traceback.format_exc())}</pre>')]
|
|
216
219
|
sys.stdout = old_stdout
|
|
@@ -234,6 +237,8 @@ class ExecCmd(cmdbox_web_load_cmd.LoadCmd):
|
|
|
234
237
|
except:
|
|
235
238
|
ret = ret_main
|
|
236
239
|
if nothread:
|
|
240
|
+
if isinstance(ret, str):
|
|
241
|
+
return PlainTextResponse(ret, media_type='text/plain')
|
|
237
242
|
return ret
|
|
238
243
|
self.callback_return_cmd_exec_func(web, title, ret)
|
|
239
244
|
except:
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import urllib.parse
|
|
2
1
|
from cmdbox.app import feature
|
|
2
|
+
from cmdbox.app.auth import signin
|
|
3
3
|
from cmdbox.app.web import Web
|
|
4
4
|
from fastapi import FastAPI, Request, Response, HTTPException
|
|
5
5
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
6
6
|
import urllib
|
|
7
|
+
import urllib.parse
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class Signin(feature.WebFeature):
|
|
@@ -22,10 +23,9 @@ class Signin(feature.WebFeature):
|
|
|
22
23
|
with open(web.signin_html, 'r', encoding='utf-8') as f:
|
|
23
24
|
web.signin_html_data = f.read()
|
|
24
25
|
|
|
25
|
-
@app.
|
|
26
|
-
@app.post('/signin/{next}', response_class=HTMLResponse)
|
|
26
|
+
@app.api_route('/signin/{next}', methods=['GET', 'POST'], response_class=HTMLResponse)
|
|
27
27
|
async def _signin(next:str, req:Request, res:Response):
|
|
28
|
-
|
|
28
|
+
signin.Signin._enable_cors(req, res)
|
|
29
29
|
res.headers['Access-Control-Allow-Origin'] = '*'
|
|
30
30
|
return web.signin_html_data
|
|
31
31
|
|
|
@@ -83,3 +83,25 @@ class Signin(feature.WebFeature):
|
|
|
83
83
|
return dict(google=signin_data['oauth2']['providers']['google']['enabled'],
|
|
84
84
|
github=signin_data['oauth2']['providers']['github']['enabled'],
|
|
85
85
|
azure=signin_data['oauth2']['providers']['azure']['enabled'],)
|
|
86
|
+
|
|
87
|
+
@app.get('/saml/{prov}/{next}')
|
|
88
|
+
async def saml_login(prov:str, next:str, req:Request, res:Response):
|
|
89
|
+
"""
|
|
90
|
+
SAML認証のログイン処理を行います
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
prov (str): SAMLプロバイダ名
|
|
94
|
+
next (str): リダイレクト先のURL
|
|
95
|
+
req (Request): Requestオブジェクト
|
|
96
|
+
res (Response): Responseオブジェクト
|
|
97
|
+
"""
|
|
98
|
+
form = await req.form()
|
|
99
|
+
auth = await web.signin_saml.make_saml(prov, next, form, req, res)
|
|
100
|
+
return RedirectResponse(url=auth.login())
|
|
101
|
+
|
|
102
|
+
@app.get('/saml/enabled')
|
|
103
|
+
async def saml_enabled(req:Request, res:Response):
|
|
104
|
+
if web.signin_html_data is None:
|
|
105
|
+
return dict(azure=False)
|
|
106
|
+
signin_data = web.signin_saml.get_data()
|
|
107
|
+
return dict(azure=signin_data['saml']['providers']['azure']['enabled'],)
|
|
@@ -88,6 +88,7 @@ class Users(feature.WebFeature):
|
|
|
88
88
|
except Exception as e:
|
|
89
89
|
return dict(error=str(e))
|
|
90
90
|
|
|
91
|
+
@app.post('/gui/apikey/add')
|
|
91
92
|
@app.post('/users/apikey/add')
|
|
92
93
|
async def users_apikey_add(req:Request, res:Response):
|
|
93
94
|
signin = web.signin.check_signin(req, res)
|
|
@@ -103,6 +104,7 @@ class Users(feature.WebFeature):
|
|
|
103
104
|
except Exception as e:
|
|
104
105
|
return dict(error=str(e))
|
|
105
106
|
|
|
107
|
+
@app.post('/gui/apikey/del')
|
|
106
108
|
@app.post('/users/apikey/del')
|
|
107
109
|
async def users_apikey_del(req:Request, res:Response):
|
|
108
110
|
signin = web.signin.check_signin(req, res)
|