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.

Files changed (188) hide show
  1. cmdbox/app/auth/__init__.py +0 -0
  2. cmdbox/app/auth/azure_signin.py +38 -0
  3. cmdbox/app/auth/azure_signin_saml.py +12 -0
  4. cmdbox/app/auth/github_signin.py +38 -0
  5. cmdbox/app/auth/google_signin.py +32 -0
  6. cmdbox/app/auth/signin.py +490 -287
  7. cmdbox/app/auth/signin_saml.py +61 -0
  8. cmdbox/app/common.py +48 -3
  9. cmdbox/app/edge.py +182 -213
  10. cmdbox/app/edge_tool.py +177 -0
  11. cmdbox/app/feature.py +10 -10
  12. cmdbox/app/features/cli/agent_base.py +477 -0
  13. cmdbox/app/features/cli/audit_base.py +1 -1
  14. cmdbox/app/features/cli/cmdbox_audit_search.py +24 -1
  15. cmdbox/app/features/cli/cmdbox_client_file_download.py +1 -1
  16. cmdbox/app/features/cli/cmdbox_cmd_list.py +105 -0
  17. cmdbox/app/features/cli/cmdbox_cmd_load.py +104 -0
  18. cmdbox/app/features/cli/cmdbox_edge_config.py +21 -7
  19. cmdbox/app/features/cli/cmdbox_edge_start.py +1 -1
  20. cmdbox/app/features/cli/cmdbox_gui_start.py +9 -132
  21. cmdbox/app/features/cli/cmdbox_gui_stop.py +4 -21
  22. cmdbox/app/features/cli/cmdbox_server_start.py +1 -1
  23. cmdbox/app/features/cli/cmdbox_web_apikey_add.py +1 -1
  24. cmdbox/app/features/cli/cmdbox_web_apikey_del.py +1 -1
  25. cmdbox/app/features/cli/cmdbox_web_genpass.py +0 -3
  26. cmdbox/app/features/cli/cmdbox_web_group_add.py +1 -1
  27. cmdbox/app/features/cli/cmdbox_web_group_del.py +1 -1
  28. cmdbox/app/features/cli/cmdbox_web_group_edit.py +1 -1
  29. cmdbox/app/features/cli/cmdbox_web_group_list.py +1 -1
  30. cmdbox/app/features/cli/cmdbox_web_start.py +119 -104
  31. cmdbox/app/features/cli/cmdbox_web_stop.py +1 -1
  32. cmdbox/app/features/cli/cmdbox_web_user_add.py +4 -4
  33. cmdbox/app/features/cli/cmdbox_web_user_del.py +1 -1
  34. cmdbox/app/features/cli/cmdbox_web_user_edit.py +4 -4
  35. cmdbox/app/features/cli/cmdbox_web_user_list.py +1 -1
  36. cmdbox/app/features/web/cmdbox_web_agent.py +250 -0
  37. cmdbox/app/features/web/cmdbox_web_do_signin.py +79 -103
  38. cmdbox/app/features/web/cmdbox_web_exec_cmd.py +8 -3
  39. cmdbox/app/features/web/cmdbox_web_signin.py +26 -4
  40. cmdbox/app/features/web/cmdbox_web_users.py +2 -0
  41. cmdbox/app/options.py +55 -2
  42. cmdbox/app/web.py +155 -27
  43. cmdbox/extensions/features.yml +18 -0
  44. cmdbox/extensions/sample_project/sample/app/features/cli/__init__.py +0 -0
  45. cmdbox/extensions/sample_project/sample/app/features/web/__init__.py +0 -0
  46. cmdbox/extensions/sample_project/sample/extensions/features.yml +23 -0
  47. cmdbox/extensions/sample_project/sample/extensions/user_list.yml +40 -6
  48. cmdbox/extensions/user_list.yml +37 -6
  49. cmdbox/licenses/{LICENSE.starlette.0.41.3(BSD License).txt → LICENSE.Authlib.1.5.2(BSD License).txt } +3 -1
  50. cmdbox/licenses/{LICENSE.pydantic_core.2.33.0(MIT License).txt → LICENSE.Deprecated.1.2.18(MIT License).txt } +2 -2
  51. cmdbox/licenses/{LICENSE.more-itertools.10.6.0(MIT License).txt → LICENSE.SQLAlchemy.2.0.40(MIT License).txt } +1 -1
  52. cmdbox/licenses/LICENSE.aiohttp.3.11.18(Apache Software License).txt +13 -0
  53. cmdbox/licenses/LICENSE.aiosignal.1.3.2(Apache Software License).txt +201 -0
  54. cmdbox/licenses/LICENSE.async-timeout.5.0.1(Apache Software License).txt +13 -0
  55. cmdbox/licenses/{LICENSE.watchfiles.1.0.0(MIT License).txt → LICENSE.attrs.25.3.0(UNKNOWN).txt} +1 -1
  56. cmdbox/licenses/{LICENSE.anyio.4.6.2.post1(MIT License).txt → LICENSE.cachetools.5.5.2(MIT License).txt } +1 -1
  57. cmdbox/licenses/LICENSE.distro.1.9.0(Apache Software License).txt +202 -0
  58. cmdbox/licenses/{LICENSE.pydantic_core.2.33.1(MIT License).txt → LICENSE.docstring_parser.0.16(MIT License).txt } +1 -1
  59. cmdbox/licenses/LICENSE.filelock.3.18.0(The Unlicense (Unlicense)).txt +24 -0
  60. cmdbox/licenses/LICENSE.frozenlist.1.6.0(Apache-2.0).txt +201 -0
  61. cmdbox/licenses/{LICENSE.starlette.0.46.1(BSD License).txt → LICENSE.fsspec.2025.3.2(BSD License).txt } +3 -1
  62. cmdbox/licenses/{LICENSE.argcomplete.3.6.1(Apache Software License).txt → LICENSE.google-adk.0.5.0(Apache Software License).txt } +25 -0
  63. cmdbox/licenses/LICENSE.google-api-python-client.2.169.0(Apache Software License).txt +201 -0
  64. cmdbox/licenses/LICENSE.google-auth-httplib2.0.2.0(Apache Software License).txt +201 -0
  65. cmdbox/licenses/LICENSE.google-auth.2.40.1(Apache Software License).txt +201 -0
  66. cmdbox/licenses/LICENSE.google-cloud-aiplatform.1.92.0(Apache 2.0).txt +202 -0
  67. cmdbox/licenses/LICENSE.google-cloud-bigquery.3.31.0(Apache Software License).txt +202 -0
  68. cmdbox/licenses/LICENSE.google-cloud-core.2.4.3(Apache Software License).txt +202 -0
  69. cmdbox/licenses/LICENSE.google-cloud-resource-manager.1.14.2(Apache Software License).txt +202 -0
  70. cmdbox/licenses/LICENSE.google-cloud-secret-manager.2.23.3(Apache Software License).txt +202 -0
  71. cmdbox/licenses/LICENSE.google-cloud-speech.2.32.0(Apache Software License).txt +202 -0
  72. cmdbox/licenses/LICENSE.google-cloud-storage.2.19.0(Apache Software License).txt +202 -0
  73. cmdbox/licenses/LICENSE.google-cloud-trace.1.16.1(Apache Software License).txt +202 -0
  74. cmdbox/licenses/LICENSE.google-crc32c.1.7.1(Apache 2.0).txt +202 -0
  75. cmdbox/licenses/LICENSE.google-genai.1.14.0(Apache Software License).txt +202 -0
  76. cmdbox/licenses/LICENSE.google-resumable-media.2.7.2(Apache Software License).txt +202 -0
  77. cmdbox/licenses/LICENSE.googleapis-common-protos.1.70.0(Apache Software License).txt +202 -0
  78. cmdbox/licenses/{LICENSE.fastapi.0.115.5(MIT License).txt → LICENSE.graphviz.0.20.3(MIT License).txt } +1 -1
  79. cmdbox/licenses/LICENSE.grpc-google-iam-v1.0.14.2(Apache Software License).txt +202 -0
  80. cmdbox/licenses/LICENSE.grpcio-status.1.71.0(Apache Software License).txt +610 -0
  81. cmdbox/licenses/LICENSE.grpcio.1.71.0(Apache Software License).txt +610 -0
  82. cmdbox/licenses/{LICENSE.uvicorn.0.34.0(BSD License).txt → LICENSE.httpcore.1.0.9(BSD License).txt } +1 -1
  83. cmdbox/licenses/LICENSE.httplib2.0.22.0(MIT License).txt +23 -0
  84. cmdbox/licenses/{LICENSE.tomli.2.1.0(MIT License).txt → LICENSE.httpx-sse.0.4.0(MIT).txt} +1 -1
  85. cmdbox/licenses/LICENSE.httpx.0.28.1(BSD License).txt +12 -0
  86. cmdbox/licenses/LICENSE.huggingface-hub.0.31.1(Apache Software License).txt +201 -0
  87. cmdbox/licenses/{LICENSE.charset-normalizer.3.4.0(MIT License).txt → LICENSE.jsonschema-specifications.2025.4.1(UNKNOWN).txt} +5 -7
  88. cmdbox/licenses/LICENSE.jsonschema.4.23.0(MIT License).txt +19 -0
  89. cmdbox/licenses/{LICENSE.pkginfo.1.10.0(MIT License).txt → LICENSE.litellm.1.69.0(MIT License).txt } +6 -1
  90. cmdbox/licenses/{LICENSE.redis.5.2.1(MIT License).txt → LICENSE.mcp.1.8.0(MIT License).txt } +1 -1
  91. cmdbox/licenses/LICENSE.multidict.6.4.3(Apache Software License).txt +13 -0
  92. cmdbox/licenses/{LICENSE.argcomplete.3.5.1(Apache Software License).txt → LICENSE.openai.1.75.0(Apache Software License).txt } +25 -1
  93. cmdbox/licenses/LICENSE.opentelemetry-api.1.33.0(Apache Software License).txt +201 -0
  94. cmdbox/licenses/LICENSE.opentelemetry-exporter-gcp-trace.1.9.0(Apache Software License).txt +201 -0
  95. cmdbox/licenses/LICENSE.opentelemetry-resourcedetector-gcp.1.9.0a0(Apache Software License).txt +201 -0
  96. cmdbox/licenses/LICENSE.opentelemetry-sdk.1.33.0(Apache Software License).txt +201 -0
  97. cmdbox/licenses/LICENSE.opentelemetry-semantic-conventions.0.54b0(Apache Software License).txt +201 -0
  98. cmdbox/licenses/LICENSE.propcache.0.3.1(Apache Software License).txt +202 -0
  99. cmdbox/licenses/LICENSE.proto-plus.1.26.1(Apache Software License).txt +202 -0
  100. cmdbox/licenses/{LICENSE.Pygments.2.18.0(BSD License).txt → LICENSE.protobuf.5.29.4(3-Clause BSD License).txt } +15 -8
  101. cmdbox/licenses/LICENSE.pyasn1.0.6.1(BSD License).txt +24 -0
  102. cmdbox/licenses/LICENSE.pyasn1_modules.0.4.2(BSD License).txt +24 -0
  103. cmdbox/licenses/LICENSE.pydantic-settings.2.9.1(MIT License).txt +21 -0
  104. cmdbox/licenses/{LICENSE.gevent.25.4.1(MIT).txt → LICENSE.pyparsing.3.2.3(MIT License).txt } +5 -12
  105. cmdbox/licenses/LICENSE.python-dateutil.2.9.0.post0(Apache Software License; BSD License).txt +54 -0
  106. cmdbox/licenses/LICENSE.referencing.0.36.2(UNKNOWN).txt +19 -0
  107. cmdbox/licenses/LICENSE.regex.2024.11.6(Apache Software License).txt +208 -0
  108. cmdbox/licenses/LICENSE.rpds-py.0.24.0(MIT).txt +19 -0
  109. cmdbox/licenses/{LICENSE.python-multipart.0.0.17(Apache Software License).txt → LICENSE.rsa.4.9.1(Apache Software License).txt } +1 -2
  110. cmdbox/licenses/{LICENSE.sphinx-intl.2.3.0(BSD License).txt → LICENSE.shapely.2.1.0(BSD License).txt } +6 -2
  111. cmdbox/licenses/LICENSE.sse-starlette.2.3.4(BSD License).txt +27 -0
  112. cmdbox/licenses/LICENSE.tiktoken.0.9.0(MIT License).txt +21 -0
  113. cmdbox/licenses/LICENSE.tokenizers.0.21.1(Apache Software License).txt +1 -0
  114. 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
  115. cmdbox/licenses/{LICENSE.rich.13.9.4(MIT License).txt → LICENSE.tzlocal.5.3.1(MIT License).txt } +3 -3
  116. cmdbox/licenses/LICENSE.uritemplate.4.1.1(Apache Software License; BSD License).txt +3 -0
  117. cmdbox/licenses/LICENSE.wrapt.1.17.2(BSD License).txt +24 -0
  118. cmdbox/licenses/LICENSE.yarl.1.20.0(Apache Software License).txt +202 -0
  119. cmdbox/licenses/files.txt +111 -17
  120. cmdbox/logconf_agent.yml +38 -0
  121. cmdbox/logconf_audit.yml +13 -5
  122. cmdbox/logconf_client.yml +13 -5
  123. cmdbox/logconf_cmdbox.yml +13 -5
  124. cmdbox/logconf_edge.yml +13 -5
  125. cmdbox/logconf_gui.yml +13 -5
  126. cmdbox/logconf_server.yml +13 -5
  127. cmdbox/logconf_web.yml +13 -5
  128. cmdbox/version.py +3 -2
  129. cmdbox/web/agent.html +263 -0
  130. cmdbox/web/assets/cmdbox/agent.js +335 -0
  131. cmdbox/web/assets/cmdbox/common.js +1111 -1020
  132. cmdbox/web/assets/cmdbox/signin.js +16 -3
  133. cmdbox/web/assets/cmdbox/users.js +1 -1
  134. cmdbox/web/assets/filer/filer.js +4 -2
  135. cmdbox/web/signin.html +10 -6
  136. {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/METADATA +132 -35
  137. {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/RECORD +161 -123
  138. cmdbox/app/features/web/cmdbox_web_load_pin.py +0 -43
  139. cmdbox/app/features/web/cmdbox_web_save_pin.py +0 -42
  140. cmdbox/licenses/LICENSE.Jinja2.3.1.4(BSD License).txt +0 -28
  141. cmdbox/licenses/LICENSE.Sphinx.8.1.3(BSD License).txt +0 -31
  142. cmdbox/licenses/LICENSE.babel.2.16.0(BSD License).txt +0 -27
  143. cmdbox/licenses/LICENSE.certifi.2025.1.31(Mozilla Public License 2.0 (MPL 2.0)).txt +0 -20
  144. cmdbox/licenses/LICENSE.click.8.1.8(BSD License).txt +0 -28
  145. cmdbox/licenses/LICENSE.cryptography.44.0.2(Apache Software License; BSD License).txt +0 -3
  146. cmdbox/licenses/LICENSE.greenlet.3.2.0(MIT AND Python-2.0).txt +0 -30
  147. cmdbox/licenses/LICENSE.keyring.25.5.0(MIT License).txt +0 -17
  148. cmdbox/licenses/LICENSE.numpy.2.2.4(BSD License).txt +0 -950
  149. cmdbox/licenses/LICENSE.pillow.11.0.0(CMU License (MIT-CMU)).txt +0 -1226
  150. cmdbox/licenses/LICENSE.pillow.11.1.0(CMU License (MIT-CMU)).txt +0 -1213
  151. cmdbox/licenses/LICENSE.prettytable.3.12.0(BSD License).txt +0 -30
  152. cmdbox/licenses/LICENSE.prompt_toolkit.3.0.50(BSD License).txt +0 -27
  153. cmdbox/licenses/LICENSE.psycopg.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +0 -165
  154. cmdbox/licenses/LICENSE.pydantic.2.11.1(MIT License).txt +0 -21
  155. cmdbox/licenses/LICENSE.pydantic.2.11.3(MIT License).txt +0 -21
  156. cmdbox/licenses/LICENSE.python-dotenv.1.0.1(BSD License).txt +0 -27
  157. cmdbox/licenses/LICENSE.twine.5.1.1(Apache Software License).txt +0 -174
  158. cmdbox/licenses/LICENSE.typing_extensions.4.13.0(UNKNOWN).txt +0 -279
  159. cmdbox/licenses/LICENSE.urllib3.2.2.3(MIT License).txt +0 -21
  160. cmdbox/licenses/LICENSE.urllib3.2.3.0(MIT License).txt +0 -21
  161. cmdbox/licenses/LICENSE.uvicorn.0.34.1(BSD License).txt +0 -27
  162. cmdbox/licenses/LICENSE.watchfiles.1.0.4(MIT License).txt +0 -21
  163. cmdbox/licenses/LICENSE.websockets.14.1(BSD License).txt +0 -24
  164. cmdbox/licenses/LICENSE.zope.interface.7.1.1(Zope Public License).txt +0 -44
  165. /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
  166. /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
  167. /cmdbox/licenses/{LICENSE.charset-normalizer.3.4.1(MIT License).txt → LICENSE.charset-normalizer.3.4.2(MIT License).txt} +0 -0
  168. /cmdbox/licenses/{LICENSE.click.8.1.7(BSD License).txt → LICENSE.click.8.2.0(UNKNOWN).txt} +0 -0
  169. /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
  170. /cmdbox/licenses/{LICENSE.gevent.24.11.1(MIT License).txt → LICENSE.gevent.25.4.2(MIT).txt} +0 -0
  171. /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
  172. /cmdbox/licenses/{LICENSE.greenlet.3.1.1(MIT License).txt → LICENSE.greenlet.3.2.2(MIT AND Python-2.0).txt} +0 -0
  173. /cmdbox/licenses/{LICENSE.h11.0.14.0(MIT License).txt → LICENSE.h11.0.16.0(MIT License).txt} +0 -0
  174. /cmdbox/licenses/{LICENSE.nh3.0.2.18(MIT).txt → LICENSE.jiter.0.9.0(MIT License).txt} +0 -0
  175. /cmdbox/licenses/{LICENSE.more-itertools.10.5.0(MIT License).txt → LICENSE.more-itertools.10.7.0(MIT License).txt} +0 -0
  176. /cmdbox/licenses/{LICENSE.numpy.2.1.3(BSD License).txt → LICENSE.numpy.2.2.5(BSD License).txt} +0 -0
  177. /cmdbox/licenses/{LICENSE.packaging.24.2(Apache Software License; BSD License).txt → LICENSE.packaging.25.0(Apache Software License; BSD License).txt} +0 -0
  178. /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
  179. /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
  180. /cmdbox/licenses/{LICENSE.pydantic.2.10.2(MIT License).txt → LICENSE.pydantic.2.11.4(MIT License).txt} +0 -0
  181. /cmdbox/licenses/{LICENSE.pydantic_core.2.27.1(MIT License).txt → LICENSE.pydantic_core.2.33.2(MIT License).txt} +0 -0
  182. /cmdbox/licenses/{LICENSE.redis.5.2.0(MIT License).txt → LICENSE.redis.6.0.0(MIT License).txt} +0 -0
  183. /cmdbox/licenses/{LICENSE.snowballstemmer.2.2.0(BSD License).txt → LICENSE.snowballstemmer.3.0.1(BSD License).txt} +0 -0
  184. /cmdbox/licenses/{LICENSE.uvicorn.0.32.1(BSD License).txt → LICENSE.uvicorn.0.34.2(BSD License).txt} +0 -0
  185. {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/LICENSE +0 -0
  186. {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/WHEEL +0 -0
  187. {cmdbox-0.5.3.1.dist-info → cmdbox-0.6.0.dist-info}/entry_points.txt +0 -0
  188. {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.signin import Signin
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 requests
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 = Signin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
165
- self.github_signin = Signin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
166
- self.azure_signin = Signin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
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
- token_resp = requests.post(url='https://oauth2.googleapis.com/token', headers=headers, data=query)
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
- try:
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
- token_resp = requests.post(url='https://github.com/login/oauth/access_token', headers=headers, data=query)
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
- try:
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
- token_resp = requests.post(url=f'https://login.microsoftonline.com/{conf["tenant_id"]}/oauth2/v2.0/token', headers=headers, data=query)
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
- user_info_resp = requests.get(
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 = self.azure_signin.jadge(access_token, email)
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 = self.azure_signin.get_groups(access_token, user)
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
- old_stdout.write(loghandler.colorize_msg(f'EXEC: {opt_list}\n'[:logsize]))
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
- old_stdout.write(loghandler.colorize_msg(f'EXEC: {output}'[:logsize])+'\n')
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
- old_stdout.write(loghandler.colorize_msg(f'EXEC: {msg}'[:logsize])+'\n')
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.get('/signin/{next}', response_class=HTMLResponse)
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
- web.signin.enable_cors(req, res)
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)