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
cmdbox/app/auth/signin.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from cmdbox.app import common, options
2
- from fastapi import Request, Response, HTTPException
2
+ from fastapi import Request, Response, HTTPException, WebSocket
3
3
  from fastapi.responses import RedirectResponse
4
4
  from pathlib import Path
5
5
  from typing import Dict, Any, Tuple, List, Union
6
6
  import copy
7
+ import contextvars
7
8
  import logging
8
9
  import string
9
10
 
@@ -27,13 +28,12 @@ class Signin(object):
27
28
  """
28
29
  return self.signin_file_data
29
30
 
30
- def jadge(self, access_token:str, email:str) -> Tuple[bool, Dict[str, Any]]:
31
+ def jadge(self, email:str) -> Tuple[bool, Dict[str, Any]]:
31
32
  """
32
33
  サインインを成功させるかどうかを判定します。
33
34
  返すユーザーデータには、uid, name, email, groups, hash が必要です。
34
35
 
35
36
  Args:
36
- access_token (str): アクセストークン
37
37
  email (str): メールアドレス
38
38
 
39
39
  Returns:
@@ -60,7 +60,8 @@ class Signin(object):
60
60
  gids = [g['gid'] for g in copy_signin_data['groups'] if g['name'] in group_names]
61
61
  return group_names, gids
62
62
 
63
- def enable_cors(self, req:Request, res:Response) -> None:
63
+ @classmethod
64
+ def _enable_cors(cls, req:Request, res:Response) -> None:
64
65
  """
65
66
  CORSを有効にする
66
67
 
@@ -72,7 +73,7 @@ class Signin(object):
72
73
  return
73
74
  res.headers['Access-Control-Allow-Origin'] = res.headers['Origin']
74
75
 
75
- def check_signin(self, req:Request, res:Response):
76
+ def check_signin(self, req:Request, res:Response) -> Union[None, RedirectResponse]:
76
77
  """
77
78
  サインインをチェックする
78
79
 
@@ -81,24 +82,44 @@ class Signin(object):
81
82
  res (Response): レスポンス
82
83
 
83
84
  Returns:
84
- Response: サインインエラーの場合はリダイレクトレスポンス
85
+ Union[None, RedirectResponse]: サインインエラーの場合はリダイレクトレスポンス
85
86
  """
86
- self.enable_cors(req, res)
87
87
  if self.signin_file_data is None:
88
88
  return None
89
89
  if 'signin' in req.session:
90
- self.signin_file_data = self.load_signin_file(self.signin_file, self.signin_file_data) # サインインファイルの更新をチェック
91
- path_jadge = self.check_path(req, req.url.path)
90
+ self.signin_file_data = Signin.load_signin_file(self.signin_file, self.signin_file_data) # サインインファイルの更新をチェック
91
+ return Signin._check_signin(req, res, self.signin_file_data, self.logger)
92
+
93
+ @classmethod
94
+ def _check_signin(cls, req:Request, res:Response, signin_file_data:Dict[str, Any], logger:logging.Logger) -> Union[None, RedirectResponse]:
95
+ """
96
+ サインインをチェックする
97
+
98
+ Args:
99
+ req (Request): リクエスト
100
+ res (Response): レスポンス
101
+ signin_file_data (Dict[str, Any]): サインインファイルデータ(変更不可)
102
+ logger (logging.Logger): ロガー
103
+
104
+ Returns:
105
+ Union[None, RedirectResponse]: サインインエラーの場合はリダイレクトレスポンス
106
+ """
107
+ Signin._enable_cors(req, res)
108
+ if signin_file_data is None:
109
+ return None
110
+ if 'signin' in req.session:
111
+ path_jadge = Signin._check_path(req, req.url.path, signin_file_data, logger)
92
112
  if path_jadge is not None:
93
113
  return path_jadge
94
114
  return None
95
- self.logger.info(f"Not found siginin session. Try check_apikey. path={req.url.path}")
96
- ret = self.check_apikey(req, res)
97
- if ret is not None and self.logger.level == logging.DEBUG:
98
- self.logger.debug(f"Not signed in.")
115
+ if logger.level == logging.DEBUG:
116
+ logger.debug(f"Not found siginin session. Try check_apikey. path={req.url.path}")
117
+ ret = Signin._check_apikey(req, res, signin_file_data, logger)
118
+ if ret is not None and logger.level == logging.DEBUG:
119
+ logger.debug(f"Not signed in.")
99
120
  return ret
100
121
 
101
- def check_apikey(self, req:Request, res:Response):
122
+ def check_apikey(self, req:Request, res:Response) -> Union[None, RedirectResponse]:
102
123
  """
103
124
  ApiKeyをチェックする
104
125
 
@@ -107,56 +128,71 @@ class Signin(object):
107
128
  res (Response): レスポンス
108
129
 
109
130
  Returns:
110
- Response: サインインエラーの場合はリダイレクトレスポンス
131
+ Union[None, RedirectResponse]: サインインエラーの場合はリダイレクトレスポンス
111
132
  """
112
- self.enable_cors(req, res)
113
- if self.signin_file_data is None:
133
+ return Signin._check_apikey(req, res, self.signin_file_data)
134
+
135
+ @classmethod
136
+ def _check_apikey(cls, req:Request, res:Response, signin_file_data:Dict[str, Any], logger:logging.Logger) -> Union[None, RedirectResponse]:
137
+ """
138
+ ApiKeyをチェックする
139
+
140
+ Args:
141
+ req (Request): リクエスト
142
+ res (Response): レスポンス
143
+ signin_file_data (Dict[str, Any]): サインインファイルデータ(変更不可)
144
+ logger (logging.Logger): ロガー
145
+
146
+ Returns:
147
+ Union[None, RedirectResponse]: サインインエラーの場合はリダイレクトレスポンス
148
+ """
149
+ Signin._enable_cors(req, res)
150
+ if signin_file_data is None:
114
151
  res.headers['signin'] = 'success'
115
152
  return None
116
153
  if 'Authorization' not in req.headers:
117
- self.logger.warning(f"Authorization not found. headers={req.headers}")
154
+ #self.logger.warning(f"Authorization not found. headers={req.headers}")
118
155
  return RedirectResponse(url=f'/signin{req.url.path}?error=noauth')
119
156
  auth = req.headers['Authorization']
120
157
  if not auth.startswith('Bearer '):
121
- self.logger.warning(f"Bearer not found. headers={req.headers}")
158
+ #self.logger.warning(f"Bearer not found. headers={req.headers}")
122
159
  return RedirectResponse(url=f'/signin{req.url.path}?error=apikeyfail')
123
160
  bearer, apikey = auth.split(' ')
124
161
  apikey = common.hash_password(apikey.strip(), 'sha1')
125
- if self.logger.level == logging.DEBUG:
126
- self.logger.debug(f"hashed apikey: {apikey}")
162
+ if logger.level == logging.DEBUG:
163
+ logger.debug(f"hashed apikey: {apikey}")
127
164
  find_user = None
128
- self.signin_file_data = self.load_signin_file(self.signin_file, self.signin_file_data) # サインインファイルの更新をチェック
129
- for user in self.signin_file_data['users']:
165
+ for user in signin_file_data['users']:
130
166
  if 'apikeys' not in user:
131
167
  continue
132
168
  for ak, key in user['apikeys'].items():
133
169
  if apikey == key:
134
170
  find_user = user
135
171
  if find_user is None:
136
- self.logger.warning(f"No matching user found for apikey.")
172
+ logger.warning(f"No matching user found for apikey.")
137
173
  return RedirectResponse(url=f'/signin{req.url.path}?error=apikeyfail')
138
174
 
139
- group_names = list(set(self.__class__.correct_group(self.get_data(), find_user['groups'], None)))
140
- gids = [g['gid'] for g in self.signin_file_data['groups'] if g['name'] in group_names]
175
+ group_names = list(set(Signin.correct_group(signin_file_data, find_user['groups'], None)))
176
+ gids = [g['gid'] for g in signin_file_data['groups'] if g['name'] in group_names]
141
177
  req.session['signin'] = dict(uid=find_user['uid'], name=find_user['name'], password=find_user['password'],
142
178
  gids=gids, groups=group_names)
143
- if self.logger.level == logging.DEBUG:
144
- self.logger.debug(f"find user: name={find_user['name']}, group_names={group_names}")
179
+ if logger.level == logging.DEBUG:
180
+ logger.debug(f"find user: name={find_user['name']}, group_names={group_names}")
145
181
  # パスルールチェック
146
182
  user_groups = find_user['groups']
147
- jadge = self.signin_file_data['pathrule']['policy']
148
- for rule in self.signin_file_data['pathrule']['rules']:
183
+ jadge = signin_file_data['pathrule']['policy']
184
+ for rule in signin_file_data['pathrule']['rules']:
149
185
  if len([g for g in rule['groups'] if g in user_groups]) <= 0:
150
186
  continue
151
187
  if len([p for p in rule['paths'] if req.url.path.startswith(p)]) <= 0:
152
188
  continue
153
189
  jadge = rule['rule']
154
- if self.logger.level == logging.DEBUG:
155
- self.logger.debug(f"rule: {req.url.path}: {jadge}")
190
+ if logger.level == logging.DEBUG:
191
+ logger.debug(f"rule: {req.url.path}: {jadge}")
156
192
  if jadge == 'allow':
157
193
  res.headers['signin'] = 'success'
158
194
  return None
159
- self.logger.warning(f"Unauthorized site. user={find_user['name']}, path={req.url.path}")
195
+ logger.warning(f"Unauthorized site. user={find_user['name']}, path={req.url.path}")
160
196
  return RedirectResponse(url=f'/signin{req.url.path}?error=unauthorizedsite')
161
197
 
162
198
  @classmethod
@@ -174,250 +210,270 @@ class Signin(object):
174
210
  Returns:
175
211
  Dict[str, Any]: サインインファイルデータ
176
212
  """
177
- if signin_file is not None:
178
- if not signin_file.is_file():
179
- raise HTTPException(status_code=500, detail=f'signin_file is not found. ({signin_file})')
180
- # サインインファイル読込み済みなら返すが、別プロセスがサインインファイルを更新していたら読込みを実施する。
181
- if not hasattr(cls, 'signin_file_last'):
182
- cls.signin_file_last = signin_file.stat().st_mtime
183
- if cls.signin_file_last >= signin_file.stat().st_mtime and signin_file_data is not None:
184
- return signin_file_data
213
+ if signin_file is None:
214
+ return None
215
+ signin_file = Path(signin_file) if isinstance(signin_file, str) else signin_file
216
+ if not signin_file.is_file():
217
+ raise HTTPException(status_code=500, detail=f'signin_file is not found. ({signin_file})')
218
+ # サインインファイル読込み済みなら返すが、別プロセスがサインインファイルを更新していたら読込みを実施する。
219
+ if not hasattr(cls, 'signin_file_last'):
185
220
  cls.signin_file_last = signin_file.stat().st_mtime
186
- yml = common.load_yml(signin_file)
187
- # usersのフォーマットチェック
188
- if 'users' not in yml:
189
- raise HTTPException(status_code=500, detail=f'signin_file format error. "users" not found. ({signin_file})')
190
- uids = set()
191
- unames = set()
192
- groups = [g['name'] for g in yml['groups']]
193
- for user in yml['users']:
194
- if 'uid' not in user or user['uid'] is None:
195
- raise HTTPException(status_code=500, detail=f'signin_file format error. "uid" not found or empty. ({signin_file})')
196
- if user['uid'] in uids:
197
- raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate uid found. ({signin_file}). uid={user["uid"]}')
198
- if 'name' not in user or user['name'] is None:
199
- raise HTTPException(status_code=500, detail=f'signin_file format error. "name" not found or empty. ({signin_file})')
200
- if user['name'] in unames:
201
- raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate name found. ({signin_file}). name={user["name"]}')
202
- if 'password' not in user:
203
- raise HTTPException(status_code=500, detail=f'signin_file format error. "password" not found or empty. ({signin_file})')
204
- if 'hash' not in user or user['hash'] is None:
205
- raise HTTPException(status_code=500, detail=f'signin_file format error. "hash" not found or empty. ({signin_file})')
206
- if user['hash'] not in ['oauth2', 'plain', 'md5', 'sha1', 'sha256']:
207
- raise HTTPException(status_code=500, detail=f'signin_file format error. Algorithms not supported. ({signin_file}). hash={user["hash"]} "oauth2", "plain", "md5", "sha1", "sha256" only.')
208
- if 'groups' not in user or type(user['groups']) is not list:
209
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found or not list type. ({signin_file})')
210
- if len([ug for ug in user['groups'] if ug not in groups]) > 0:
211
- raise HTTPException(status_code=500, detail=f'signin_file format error. Group not found. ({signin_file}). {user["groups"]}')
212
- uids.add(user['uid'])
213
- unames.add(user['name'])
214
- # groupsのフォーマットチェック
215
- if 'groups' not in yml:
216
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found. ({signin_file})')
217
- gids = set()
218
- gnames = set()
219
- for group in yml['groups']:
220
- if 'gid' not in group or group['gid'] is None:
221
- raise HTTPException(status_code=500, detail=f'signin_file format error. "gid" not found or empty. ({signin_file})')
222
- if group['gid'] in gids:
223
- raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate gid found. ({signin_file}). gid={group["gid"]}')
224
- if 'name' not in group or group['name'] is None:
225
- raise HTTPException(status_code=500, detail=f'signin_file format error. "name" not found or empty. ({signin_file})')
226
- if group['name'] in gnames:
227
- raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate name found. ({signin_file}). name={group["name"]}')
228
- if 'parent' in group:
229
- if group['parent'] not in groups:
230
- raise HTTPException(status_code=500, detail=f'signin_file format error. Parent group not found. ({signin_file}). parent={group["parent"]}')
231
- gids.add(group['gid'])
232
- gnames.add(group['name'])
233
- # cmdruleのフォーマットチェック
234
- if 'cmdrule' not in yml:
235
- raise HTTPException(status_code=500, detail=f'signin_file format error. "cmdrule" not found. ({signin_file})')
236
- if 'policy' not in yml['cmdrule']:
237
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "cmdrule". ({signin_file})')
238
- if yml['cmdrule']['policy'] not in ['allow', 'deny']:
239
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not supported in "cmdrule". ({signin_file}). "allow" or "deny" only.')
240
- if 'rules' not in yml['cmdrule']:
241
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not found in "cmdrule". ({signin_file})')
242
- if type(yml['cmdrule']['rules']) is not list:
243
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not list type in "cmdrule". ({signin_file})')
244
- for rule in yml['cmdrule']['rules']:
245
- if 'groups' not in rule:
246
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found in "cmdrule.rules" ({signin_file})')
247
- if type(rule['groups']) is not list:
248
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not list type in "cmdrule.rules". ({signin_file})')
249
- rule['groups'] = list(set(copy.deepcopy(cls.correct_group(yml, rule['groups'], yml['groups']))))
250
- if 'rule' not in rule:
251
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not found in "cmdrule.rules" ({signin_file})')
252
- if rule['rule'] not in ['allow', 'deny']:
253
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not supported in "cmdrule.rules". ({signin_file}). "allow" or "deny" only.')
254
- if 'mode' not in rule:
255
- rule['mode'] = None
256
- if 'cmds' not in rule:
257
- rule['cmds'] = []
258
- if rule['mode'] is not None and len(rule['cmds']) <= 0:
259
- raise HTTPException(status_code=500, detail=f'signin_file format error. When “cmds is specified, “mode” must be specified. ({signin_file})')
260
- if type(rule['cmds']) is not list:
261
- raise HTTPException(status_code=500, detail=f'signin_file format error. "cmds" not list type in "cmdrule.rules". ({signin_file})')
262
- # pathruleのフォーマットチェック
263
- if 'pathrule' not in yml:
264
- raise HTTPException(status_code=500, detail=f'signin_file format error. "pathrule" not found. ({signin_file})')
265
- if 'policy' not in yml['pathrule']:
266
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "pathrule". ({signin_file})')
267
- if yml['pathrule']['policy'] not in ['allow', 'deny']:
268
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not supported in "pathrule". ({signin_file}). "allow" or "deny" only.')
269
- if 'rules' not in yml['pathrule']:
270
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not found in "pathrule". ({signin_file})')
271
- if type(yml['pathrule']['rules']) is not list:
272
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not list type in "pathrule". ({signin_file})')
273
- for rule in yml['pathrule']['rules']:
274
- if 'groups' not in rule:
275
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found in "pathrule.rules" ({signin_file})')
276
- if type(rule['groups']) is not list:
277
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not list type in "pathrule.rules". ({signin_file})')
278
- rule['groups'] = list(set(copy.deepcopy(cls.correct_group(yml, rule['groups'], yml['groups']))))
279
- if 'rule' not in rule:
280
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not found in "pathrule.rules" ({signin_file})')
281
- if rule['rule'] not in ['allow', 'deny']:
282
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not supported in "pathrule.rules". ({signin_file}). "allow" or "deny" only.')
283
- if 'paths' not in rule:
284
- rule['paths'] = []
285
- if type(rule['paths']) is not list:
286
- raise HTTPException(status_code=500, detail=f'signin_file format error. "paths" not list type in "pathrule.rules". ({signin_file})')
287
- # passwordのフォーマットチェック
288
- if 'password' in yml:
289
- if 'policy' not in yml['password']:
290
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "password". ({signin_file})')
291
- if 'enabled' not in yml['password']['policy']:
292
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.policy". ({signin_file})')
293
- if type(yml['password']['policy']['enabled']) is not bool:
294
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.policy". ({signin_file})')
295
- if 'not_same_before' not in yml['password']['policy']:
296
- raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not found in "password.policy". ({signin_file})')
297
- if type(yml['password']['policy']['not_same_before']) is not bool:
298
- raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not bool type in "password.policy". ({signin_file})')
299
- if 'min_length' not in yml['password']['policy']:
300
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not found in "password.policy". ({signin_file})')
301
- if type(yml['password']['policy']['min_length']) is not int:
302
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not int type in "password.policy". ({signin_file})')
303
- if 'max_length' not in yml['password']['policy']:
304
- raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not found in "password.policy". ({signin_file})')
305
- if type(yml['password']['policy']['max_length']) is not int:
306
- raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not int type in "password.policy". ({signin_file})')
307
- if 'min_lowercase' not in yml['password']['policy']:
308
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not found in "password.policy". ({signin_file})')
309
- if type(yml['password']['policy']['min_lowercase']) is not int:
310
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not int type in "password.policy". ({signin_file})')
311
- if 'min_uppercase' not in yml['password']['policy']:
312
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not found in "password.policy". ({signin_file})')
313
- if type(yml['password']['policy']['min_uppercase']) is not int:
314
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not int type in "password.policy". ({signin_file})')
315
- if 'min_digit' not in yml['password']['policy']:
316
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not found in "password.policy". ({signin_file})')
317
- if type(yml['password']['policy']['min_digit']) is not int:
318
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not int type in "password.policy". ({signin_file})')
319
- if 'min_symbol' not in yml['password']['policy']:
320
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not found in "password.policy". ({signin_file})')
321
- if type(yml['password']['policy']['min_symbol']) is not int:
322
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not int type in "password.policy". ({signin_file})')
323
- if 'not_contain_username' not in yml['password']['policy']:
324
- raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not found in "password.policy". ({signin_file})')
325
- if type(yml['password']['policy']['not_contain_username']) is not bool:
326
- raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not bool type in "password.policy". ({signin_file})')
327
- if 'expiration' not in yml['password']:
328
- raise HTTPException(status_code=500, detail=f'signin_file format error. "expiration" not found in "password". ({signin_file})')
329
- if 'enabled' not in yml['password']['expiration']:
330
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.expiration". ({signin_file})')
331
- if type(yml['password']['expiration']['enabled']) is not bool:
332
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.expiration". ({signin_file})')
333
- if 'period' not in yml['password']['expiration']:
334
- raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not found in "password.expiration". ({signin_file})')
335
- if type(yml['password']['expiration']['period']) is not int:
336
- raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not int type in "password.expiration". ({signin_file})')
337
- if 'notify' not in yml['password']['expiration']:
338
- raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not found in "password.expiration". ({signin_file})')
339
- if type(yml['password']['expiration']['notify']) is not int:
340
- raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not int type in "password.expiration". ({signin_file})')
341
- if 'lockout' not in yml['password']:
342
- raise HTTPException(status_code=500, detail=f'signin_file format error. "lockout" not found in "password". ({signin_file})')
343
- if 'enabled' not in yml['password']['lockout']:
344
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.lockout". ({signin_file})')
345
- if type(yml['password']['lockout']['enabled']) is not bool:
346
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.lockout". ({signin_file})')
347
- if 'threshold' not in yml['password']['lockout']:
348
- raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not found in "password.lockout". ({signin_file})')
349
- if type(yml['password']['lockout']['threshold']) is not int:
350
- raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not int type in "password.lockout". ({signin_file})')
351
- if 'reset' not in yml['password']['lockout']:
352
- raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not found in "password.lockout". ({signin_file})')
353
- if type(yml['password']['lockout']['reset']) is not int:
354
- raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not int type in "password.lockout". ({signin_file})')
355
- # oauth2のフォーマットチェック
356
- if 'oauth2' not in yml:
357
- raise HTTPException(status_code=500, detail=f'signin_file format error. "oauth2" not found. ({signin_file})')
358
- if 'providers' not in yml['oauth2']:
359
- raise HTTPException(status_code=500, detail=f'signin_file format error. "providers" not found in "oauth2". ({signin_file})')
360
- # google
361
- if 'google' not in yml['oauth2']['providers']:
362
- raise HTTPException(status_code=500, detail=f'signin_file format error. "google" not found in "providers". ({signin_file})')
363
- if 'enabled' not in yml['oauth2']['providers']['google']:
364
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "google". ({signin_file})')
365
- if type(yml['oauth2']['providers']['google']['enabled']) is not bool:
366
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "google". ({signin_file})')
367
- if 'client_id' not in yml['oauth2']['providers']['google']:
368
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "google". ({signin_file})')
369
- if 'client_secret' not in yml['oauth2']['providers']['google']:
370
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "google". ({signin_file})')
371
- if 'redirect_uri' not in yml['oauth2']['providers']['google']:
372
- raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "google". ({signin_file})')
373
- if 'scope' not in yml['oauth2']['providers']['google']:
374
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "google". ({signin_file})')
375
- if type(yml['oauth2']['providers']['google']['scope']) is not list:
376
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "google". ({signin_file})')
377
- if 'signin_module' not in yml['oauth2']['providers']['google']:
378
- raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "google". ({signin_file})')
379
- # github
380
- if 'github' not in yml['oauth2']['providers']:
381
- raise HTTPException(status_code=500, detail=f'signin_file format error. "github" not found in "providers". ({signin_file})')
382
- if 'enabled' not in yml['oauth2']['providers']['github']:
383
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "github". ({signin_file})')
384
- if type(yml['oauth2']['providers']['github']['enabled']) is not bool:
385
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "github". ({signin_file})')
386
- if 'client_id' not in yml['oauth2']['providers']['github']:
387
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "github". ({signin_file})')
388
- if 'client_secret' not in yml['oauth2']['providers']['github']:
389
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "github". ({signin_file})')
390
- if 'redirect_uri' not in yml['oauth2']['providers']['github']:
391
- raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "github". ({signin_file})')
392
- if 'scope' not in yml['oauth2']['providers']['github']:
393
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "github". ({signin_file})')
394
- if type(yml['oauth2']['providers']['github']['scope']) is not list:
395
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "github". ({signin_file})')
396
- if 'signin_module' not in yml['oauth2']['providers']['github']:
397
- raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "github". ({signin_file})')
398
- # azure
399
- if 'azure' not in yml['oauth2']['providers']:
400
- raise HTTPException(status_code=500, detail=f'signin_file format error. "azure" not found in "providers". ({signin_file})')
401
- if 'enabled' not in yml['oauth2']['providers']['azure']:
402
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "azure". ({signin_file})')
403
- if type(yml['oauth2']['providers']['azure']['enabled']) is not bool:
404
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "azure". ({signin_file})')
405
- if 'tenant_id' not in yml['oauth2']['providers']['azure']:
406
- raise HTTPException(status_code=500, detail=f'signin_file format error. "tenant_id" not found in "azure". ({signin_file})')
407
- if 'client_id' not in yml['oauth2']['providers']['azure']:
408
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "azure". ({signin_file})')
409
- if 'client_secret' not in yml['oauth2']['providers']['azure']:
410
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "azure". ({signin_file})')
411
- if 'redirect_uri' not in yml['oauth2']['providers']['azure']:
412
- raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "azure". ({signin_file})')
413
- if 'scope' not in yml['oauth2']['providers']['azure']:
414
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "azure". ({signin_file})')
415
- if type(yml['oauth2']['providers']['azure']['scope']) is not list:
416
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "azure". ({signin_file})')
417
- if 'signin_module' not in yml['oauth2']['providers']['azure']:
418
- raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "azure". ({signin_file})')
419
- # フォーマットチェックOK
420
- return yml
221
+ if cls.signin_file_last >= signin_file.stat().st_mtime and signin_file_data is not None:
222
+ return signin_file_data
223
+ cls.signin_file_last = signin_file.stat().st_mtime
224
+ yml = common.load_yml(signin_file)
225
+ # usersのフォーマットチェック
226
+ if 'users' not in yml:
227
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "users" not found. ({signin_file})')
228
+ uids = set()
229
+ unames = set()
230
+ groups = [g['name'] for g in yml['groups']]
231
+ for user in yml['users']:
232
+ if 'uid' not in user or user['uid'] is None:
233
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "uid" not found or empty. ({signin_file})')
234
+ if user['uid'] in uids:
235
+ raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate uid found. ({signin_file}). uid={user["uid"]}')
236
+ if 'name' not in user or user['name'] is None:
237
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "name" not found or empty. ({signin_file})')
238
+ if user['name'] in unames:
239
+ raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate name found. ({signin_file}). name={user["name"]}')
240
+ if 'password' not in user:
241
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "password" not found or empty. ({signin_file})')
242
+ if 'hash' not in user or user['hash'] is None:
243
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "hash" not found or empty. ({signin_file})')
244
+ if user['hash'] not in ['oauth2', 'saml', 'plain', 'md5', 'sha1', 'sha256']:
245
+ raise HTTPException(status_code=500, detail=f'signin_file format error. Algorithms not supported. ({signin_file}). hash={user["hash"]} "oauth2", "saml", "plain", "md5", "sha1", "sha256" only.')
246
+ if 'groups' not in user or type(user['groups']) is not list:
247
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found or not list type. ({signin_file})')
248
+ if len([ug for ug in user['groups'] if ug not in groups]) > 0:
249
+ raise HTTPException(status_code=500, detail=f'signin_file format error. Group not found. ({signin_file}). {user["groups"]}')
250
+ uids.add(user['uid'])
251
+ unames.add(user['name'])
252
+ # groupsのフォーマットチェック
253
+ if 'groups' not in yml:
254
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found. ({signin_file})')
255
+ gids = set()
256
+ gnames = set()
257
+ for group in yml['groups']:
258
+ if 'gid' not in group or group['gid'] is None:
259
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "gid" not found or empty. ({signin_file})')
260
+ if group['gid'] in gids:
261
+ raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate gid found. ({signin_file}). gid={group["gid"]}')
262
+ if 'name' not in group or group['name'] is None:
263
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "name" not found or empty. ({signin_file})')
264
+ if group['name'] in gnames:
265
+ raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate name found. ({signin_file}). name={group["name"]}')
266
+ if 'parent' in group:
267
+ if group['parent'] not in groups:
268
+ raise HTTPException(status_code=500, detail=f'signin_file format error. Parent group not found. ({signin_file}). parent={group["parent"]}')
269
+ gids.add(group['gid'])
270
+ gnames.add(group['name'])
271
+ # cmdruleのフォーマットチェック
272
+ if 'cmdrule' not in yml:
273
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "cmdrule" not found. ({signin_file})')
274
+ if 'policy' not in yml['cmdrule']:
275
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "cmdrule". ({signin_file})')
276
+ if yml['cmdrule']['policy'] not in ['allow', 'deny']:
277
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not supported in "cmdrule". ({signin_file}). "allow" or "deny" only.')
278
+ if 'rules' not in yml['cmdrule']:
279
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not found in "cmdrule". ({signin_file})')
280
+ if type(yml['cmdrule']['rules']) is not list:
281
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not list type in "cmdrule". ({signin_file})')
282
+ for rule in yml['cmdrule']['rules']:
283
+ if 'groups' not in rule:
284
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found in "cmdrule.rules" ({signin_file})')
285
+ if type(rule['groups']) is not list:
286
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not list type in "cmdrule.rules". ({signin_file})')
287
+ rule['groups'] = list(set(copy.deepcopy(cls.correct_group(yml, rule['groups'], yml['groups']))))
288
+ if 'rule' not in rule:
289
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not found in "cmdrule.rules" ({signin_file})')
290
+ if rule['rule'] not in ['allow', 'deny']:
291
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not supported in "cmdrule.rules". ({signin_file}). "allow" or "deny" only.')
292
+ if 'mode' not in rule:
293
+ rule['mode'] = None
294
+ if 'cmds' not in rule:
295
+ rule['cmds'] = []
296
+ if rule['mode'] is None and len(rule['cmds']) > 0:
297
+ raise HTTPException(status_code=500, detail=f'signin_file format error. When “cmds” is specified, “mode” must be specified. ({signin_file})')
298
+ if type(rule['cmds']) is not list:
299
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "cmds" not list type in "cmdrule.rules". ({signin_file})')
300
+ # pathruleのフォーマットチェック
301
+ if 'pathrule' not in yml:
302
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "pathrule" not found. ({signin_file})')
303
+ if 'policy' not in yml['pathrule']:
304
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "pathrule". ({signin_file})')
305
+ if yml['pathrule']['policy'] not in ['allow', 'deny']:
306
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not supported in "pathrule". ({signin_file}). "allow" or "deny" only.')
307
+ if 'rules' not in yml['pathrule']:
308
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not found in "pathrule". ({signin_file})')
309
+ if type(yml['pathrule']['rules']) is not list:
310
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not list type in "pathrule". ({signin_file})')
311
+ for rule in yml['pathrule']['rules']:
312
+ if 'groups' not in rule:
313
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found in "pathrule.rules" ({signin_file})')
314
+ if type(rule['groups']) is not list:
315
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not list type in "pathrule.rules". ({signin_file})')
316
+ rule['groups'] = list(set(copy.deepcopy(cls.correct_group(yml, rule['groups'], yml['groups']))))
317
+ if 'rule' not in rule:
318
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not found in "pathrule.rules" ({signin_file})')
319
+ if rule['rule'] not in ['allow', 'deny']:
320
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not supported in "pathrule.rules". ({signin_file}). "allow" or "deny" only.')
321
+ if 'paths' not in rule:
322
+ rule['paths'] = []
323
+ if type(rule['paths']) is not list:
324
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "paths" not list type in "pathrule.rules". ({signin_file})')
325
+ # passwordのフォーマットチェック
326
+ if 'password' in yml:
327
+ if 'policy' not in yml['password']:
328
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "password". ({signin_file})')
329
+ if 'enabled' not in yml['password']['policy']:
330
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.policy". ({signin_file})')
331
+ if type(yml['password']['policy']['enabled']) is not bool:
332
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.policy". ({signin_file})')
333
+ if 'not_same_before' not in yml['password']['policy']:
334
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not found in "password.policy". ({signin_file})')
335
+ if type(yml['password']['policy']['not_same_before']) is not bool:
336
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not bool type in "password.policy". ({signin_file})')
337
+ if 'min_length' not in yml['password']['policy']:
338
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not found in "password.policy". ({signin_file})')
339
+ if type(yml['password']['policy']['min_length']) is not int:
340
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not int type in "password.policy". ({signin_file})')
341
+ if 'max_length' not in yml['password']['policy']:
342
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not found in "password.policy". ({signin_file})')
343
+ if type(yml['password']['policy']['max_length']) is not int:
344
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not int type in "password.policy". ({signin_file})')
345
+ if 'min_lowercase' not in yml['password']['policy']:
346
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not found in "password.policy". ({signin_file})')
347
+ if type(yml['password']['policy']['min_lowercase']) is not int:
348
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not int type in "password.policy". ({signin_file})')
349
+ if 'min_uppercase' not in yml['password']['policy']:
350
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not found in "password.policy". ({signin_file})')
351
+ if type(yml['password']['policy']['min_uppercase']) is not int:
352
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not int type in "password.policy". ({signin_file})')
353
+ if 'min_digit' not in yml['password']['policy']:
354
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not found in "password.policy". ({signin_file})')
355
+ if type(yml['password']['policy']['min_digit']) is not int:
356
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not int type in "password.policy". ({signin_file})')
357
+ if 'min_symbol' not in yml['password']['policy']:
358
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not found in "password.policy". ({signin_file})')
359
+ if type(yml['password']['policy']['min_symbol']) is not int:
360
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not int type in "password.policy". ({signin_file})')
361
+ if 'not_contain_username' not in yml['password']['policy']:
362
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not found in "password.policy". ({signin_file})')
363
+ if type(yml['password']['policy']['not_contain_username']) is not bool:
364
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not bool type in "password.policy". ({signin_file})')
365
+ if 'expiration' not in yml['password']:
366
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "expiration" not found in "password". ({signin_file})')
367
+ if 'enabled' not in yml['password']['expiration']:
368
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.expiration". ({signin_file})')
369
+ if type(yml['password']['expiration']['enabled']) is not bool:
370
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.expiration". ({signin_file})')
371
+ if 'period' not in yml['password']['expiration']:
372
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not found in "password.expiration". ({signin_file})')
373
+ if type(yml['password']['expiration']['period']) is not int:
374
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not int type in "password.expiration". ({signin_file})')
375
+ if 'notify' not in yml['password']['expiration']:
376
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not found in "password.expiration". ({signin_file})')
377
+ if type(yml['password']['expiration']['notify']) is not int:
378
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not int type in "password.expiration". ({signin_file})')
379
+ if 'lockout' not in yml['password']:
380
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "lockout" not found in "password". ({signin_file})')
381
+ if 'enabled' not in yml['password']['lockout']:
382
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.lockout". ({signin_file})')
383
+ if type(yml['password']['lockout']['enabled']) is not bool:
384
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.lockout". ({signin_file})')
385
+ if 'threshold' not in yml['password']['lockout']:
386
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not found in "password.lockout". ({signin_file})')
387
+ if type(yml['password']['lockout']['threshold']) is not int:
388
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not int type in "password.lockout". ({signin_file})')
389
+ if 'reset' not in yml['password']['lockout']:
390
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not found in "password.lockout". ({signin_file})')
391
+ if type(yml['password']['lockout']['reset']) is not int:
392
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not int type in "password.lockout". ({signin_file})')
393
+ # oauth2のフォーマットチェック
394
+ if 'oauth2' not in yml:
395
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "oauth2" not found. ({signin_file})')
396
+ if 'providers' not in yml['oauth2']:
397
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "providers" not found in "oauth2". ({signin_file})')
398
+ # google
399
+ if 'google' not in yml['oauth2']['providers']:
400
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "google" not found in "providers". ({signin_file})')
401
+ if 'enabled' not in yml['oauth2']['providers']['google']:
402
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "google". ({signin_file})')
403
+ if type(yml['oauth2']['providers']['google']['enabled']) is not bool:
404
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "google". ({signin_file})')
405
+ if 'client_id' not in yml['oauth2']['providers']['google']:
406
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "google". ({signin_file})')
407
+ if 'client_secret' not in yml['oauth2']['providers']['google']:
408
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "google". ({signin_file})')
409
+ if 'redirect_uri' not in yml['oauth2']['providers']['google']:
410
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "google". ({signin_file})')
411
+ if 'scope' not in yml['oauth2']['providers']['google']:
412
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "google". ({signin_file})')
413
+ if type(yml['oauth2']['providers']['google']['scope']) is not list:
414
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "google". ({signin_file})')
415
+ if 'signin_module' not in yml['oauth2']['providers']['google']:
416
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "google". ({signin_file})')
417
+ # github
418
+ if 'github' not in yml['oauth2']['providers']:
419
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "github" not found in "providers". ({signin_file})')
420
+ if 'enabled' not in yml['oauth2']['providers']['github']:
421
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "github". ({signin_file})')
422
+ if type(yml['oauth2']['providers']['github']['enabled']) is not bool:
423
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "github". ({signin_file})')
424
+ if 'client_id' not in yml['oauth2']['providers']['github']:
425
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "github". ({signin_file})')
426
+ if 'client_secret' not in yml['oauth2']['providers']['github']:
427
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "github". ({signin_file})')
428
+ if 'redirect_uri' not in yml['oauth2']['providers']['github']:
429
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "github". ({signin_file})')
430
+ if 'scope' not in yml['oauth2']['providers']['github']:
431
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "github". ({signin_file})')
432
+ if type(yml['oauth2']['providers']['github']['scope']) is not list:
433
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "github". ({signin_file})')
434
+ if 'signin_module' not in yml['oauth2']['providers']['github']:
435
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "github". ({signin_file})')
436
+ # azure
437
+ if 'azure' not in yml['oauth2']['providers']:
438
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "azure" not found in "providers". ({signin_file})')
439
+ if 'enabled' not in yml['oauth2']['providers']['azure']:
440
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "azure". ({signin_file})')
441
+ if type(yml['oauth2']['providers']['azure']['enabled']) is not bool:
442
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "azure". ({signin_file})')
443
+ if 'tenant_id' not in yml['oauth2']['providers']['azure']:
444
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "tenant_id" not found in "azure". ({signin_file})')
445
+ if 'client_id' not in yml['oauth2']['providers']['azure']:
446
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "azure". ({signin_file})')
447
+ if 'client_secret' not in yml['oauth2']['providers']['azure']:
448
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "azure". ({signin_file})')
449
+ if 'redirect_uri' not in yml['oauth2']['providers']['azure']:
450
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "azure". ({signin_file})')
451
+ if 'scope' not in yml['oauth2']['providers']['azure']:
452
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "azure". ({signin_file})')
453
+ if type(yml['oauth2']['providers']['azure']['scope']) is not list:
454
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "azure". ({signin_file})')
455
+ if 'signin_module' not in yml['oauth2']['providers']['azure']:
456
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "azure". ({signin_file})')
457
+ # samlのフォーマットチェック
458
+ if 'saml' not in yml:
459
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "saml" not found. ({signin_file})')
460
+ if 'providers' not in yml['saml']:
461
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "providers" not found in "saml". ({signin_file})')
462
+ # azure
463
+ if 'azure' not in yml['saml']['providers']:
464
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "azure" not found in "providers". ({signin_file})')
465
+ if 'enabled' not in yml['saml']['providers']['azure']:
466
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "azure". ({signin_file})')
467
+ if type(yml['saml']['providers']['azure']['enabled']) is not bool:
468
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "azure". ({signin_file})')
469
+ if 'signin_module' not in yml['saml']['providers']['azure']:
470
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "azure". ({signin_file})')
471
+ if 'sp' not in yml['saml']['providers']['azure']:
472
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "sp" not found in "azure". ({signin_file})')
473
+ if 'idp' not in yml['saml']['providers']['azure']:
474
+ raise HTTPException(status_code=500, detail=f'signin_file format error. "idp" not found in "azure". ({signin_file})')
475
+ # フォーマットチェックOK
476
+ return yml
421
477
 
422
478
  @classmethod
423
479
  def correct_group(cls, signin_file_data:Dict[str, Any], group_names:List[str], master_groups:List[Dict[str, Any]]) -> List[str]:
@@ -448,26 +504,42 @@ class Signin(object):
448
504
  Returns:
449
505
  Union[None, RedirectResponse]: 認可された場合はNone、認可されなかった場合はリダイレクトレスポンス
450
506
  """
451
- if self.signin_file_data is None:
507
+ return Signin._check_path(req, path, self.signin_file_data, self.logger)
508
+
509
+ @classmethod
510
+ def _check_path(cls, req:Request, path:str, signin_file_data:Dict[str, Any], logger:logging.Logger) -> Union[None, RedirectResponse]:
511
+ """
512
+ パスの認可をチェックします
513
+
514
+ Args:
515
+ req (Request): リクエスト
516
+ path (str): パス
517
+ signin_file_data (Dict[str, Any]): サインインファイルデータ
518
+ logger (logging.Logger): ロガー
519
+
520
+ Returns:
521
+ Union[None, RedirectResponse]: 認可された場合はNone、認可されなかった場合はリダイレクトレスポンス
522
+ """
523
+ if signin_file_data is None:
452
524
  return None
453
525
  if 'signin' not in req.session:
454
526
  return None
455
527
  path = path if path.startswith('/') else f'/{path}'
456
528
  # パスルールチェック
457
529
  user_groups = req.session['signin']['groups']
458
- jadge = self.signin_file_data['pathrule']['policy']
459
- for rule in self.signin_file_data['pathrule']['rules']:
530
+ jadge = signin_file_data['pathrule']['policy']
531
+ for rule in signin_file_data['pathrule']['rules']:
460
532
  if len([g for g in rule['groups'] if g in user_groups]) <= 0:
461
533
  continue
462
534
  if len([p for p in rule['paths'] if path.startswith(p)]) <= 0:
463
535
  continue
464
536
  jadge = rule['rule']
465
- if self.logger.level == logging.DEBUG:
466
- self.logger.debug(f"rule: {path}: {jadge}")
537
+ if logger.level == logging.DEBUG:
538
+ logger.debug(f"rule: {path}: {jadge}")
467
539
  if jadge == 'allow':
468
540
  return None
469
541
  else:
470
- self.logger.warning(f"Unauthorized site. user={req.session['signin']['name']}, path={path}")
542
+ logger.warning(f"Unauthorized site. user={req.session['signin']['name']}, path={path}")
471
543
  return RedirectResponse(url=f'/signin{path}?error=unauthorizedsite')
472
544
 
473
545
  def check_cmd(self, req:Request, res:Response, mode:str, cmd:str):
@@ -487,10 +559,57 @@ class Signin(object):
487
559
  return True
488
560
  if 'signin' not in req.session or 'groups' not in req.session['signin']:
489
561
  return False
562
+ return Signin._check_cmd(self.signin_file_data, req.session['signin']['groups'], mode, cmd, self.logger)
563
+
564
+ @classmethod
565
+ def load_groups(cls, signin_file_data:Dict[str, Any], apikey:str, logger:logging.Logger):
566
+ """
567
+ APIキーからユーザグループを取得します
568
+ Args:
569
+ signin_file_data (Dict[str, Any]): サインインファイルデータ
570
+ apikey (str): APIキー
571
+ logger (logging.Logger): ロガー
572
+ Returns:
573
+ Dict[str, Any]: ユーザグループの情報
574
+ """
575
+ apikey = common.hash_password(apikey.strip(), 'sha1')
576
+ if logger.level == logging.DEBUG:
577
+ logger.debug(f"hashed apikey: {apikey}")
578
+ find_user = None
579
+ for user in signin_file_data['users']:
580
+ if 'apikeys' not in user:
581
+ continue
582
+ for ak, key in user['apikeys'].items():
583
+ if apikey == key:
584
+ find_user = user
585
+ if find_user is None:
586
+ logger.warning(f"No matching user found for apikey.")
587
+ return dict(warn='No matching user found for apikey.')
588
+
589
+ group_names = list(set(Signin.correct_group(signin_file_data, find_user['groups'], None)))
590
+ return dict(success=group_names)
591
+
592
+ @classmethod
593
+ def _check_cmd(cls, signin_file_data:Dict[str, Any], user_groups:List[str], mode:str, cmd:str, logger:logging.Logger) -> bool:
594
+ """
595
+ コマンドの認可をチェックします
596
+
597
+ Args:
598
+ signin_file_data (Dict[str, Any]): サインインファイルデータ
599
+ user_groups (List[str]): ユーザグループ
600
+ mode (str): モード
601
+ cmd (str): コマンド
602
+
603
+ Returns:
604
+ bool: 認可されたかどうか
605
+ """
606
+ if signin_file_data is None:
607
+ return True
608
+ if user_groups is None or len(user_groups) <= 0:
609
+ return False
490
610
  # コマンドチェック
491
- user_groups = req.session['signin']['groups']
492
- jadge = self.signin_file_data['cmdrule']['policy']
493
- for rule in self.signin_file_data['cmdrule']['rules']:
611
+ jadge = signin_file_data['cmdrule']['policy']
612
+ for rule in signin_file_data['cmdrule']['rules']:
494
613
  if len([g for g in rule['groups'] if g in user_groups]) <= 0:
495
614
  continue
496
615
  if rule['mode'] is not None:
@@ -499,8 +618,8 @@ class Signin(object):
499
618
  if len([c for c in rule['cmds'] if cmd == c]) <= 0:
500
619
  continue
501
620
  jadge = rule['rule']
502
- if self.logger.level == logging.DEBUG:
503
- self.logger.debug(f"rule: mode={mode}, cmd={cmd}: {jadge}")
621
+ if logger.level == logging.DEBUG:
622
+ logger.debug(f"rule: mode={mode}, cmd={cmd}: {jadge}")
504
623
  return jadge == 'allow'
505
624
 
506
625
  def get_enable_modes(self, req:Request, res:Response) -> List[str]:
@@ -632,3 +751,87 @@ class Signin(object):
632
751
  return False, f"Password policy error. not_contain_username=True"
633
752
  self.logger.info(f"Password policy OK.")
634
753
  return True, "Password policy OK."
754
+
755
+ def request_access_token(self, conf:Dict, req:Request, res:Response) -> str:
756
+ """
757
+ アクセストークンを取得します
758
+
759
+ Args:
760
+ conf (Dict): サインインモジュールの設定
761
+ req (Request): リクエスト
762
+ res (Response): レスポンス
763
+
764
+ Returns:
765
+ str: アクセストークン
766
+ """
767
+ raise NotImplementedError("request_access_token() is not implemented.")
768
+
769
+ def get_email(self, data:Any) -> str:
770
+ """
771
+ アクセストークンからメールアドレスを取得します
772
+
773
+ Args:
774
+ data (str): アクセストークン又は属性データ
775
+
776
+ Returns:
777
+ str: メールアドレス
778
+ """
779
+ return self.__class__.get_email(data)
780
+
781
+
782
+ request_scope = contextvars.ContextVar('request_scope', default=None)
783
+
784
+ async def create_request_scope(req:Request=None, res:Response=None, websocket:WebSocket=None):
785
+ """
786
+ FastAPIのDepends用に、ContextVarを使用してリクエストスコープを提供します。
787
+ これにより、リクエストごとに異なるRequestオブジェクトを取得できます。
788
+ これは、FastAPIのDependsで使用されることを意図しています。
789
+ 次のように使用します。
790
+
791
+ Example:
792
+
793
+ ::
794
+
795
+ from cmdbox.app.auth import signin
796
+ from fastapi import Depends, Request, Response
797
+
798
+ @app.get("/some-endpoint")
799
+ async def some_endpoint(req: Request, res: Response, scope=Depends(signin.create_request_scope)):
800
+ # 何らかの処理
801
+
802
+ Args:
803
+ req (Request): リクエスト
804
+ res (Response): レスポンス
805
+ websocket (WebSocket): WebSocket接続
806
+ """
807
+ sess = None
808
+ if req is not None:
809
+ sess = req.session if hasattr(req, 'session') else None
810
+ request_scope.set(dict(req=req, res=res, websocket=websocket))
811
+ try:
812
+ yield # リクエストの処理
813
+ finally:
814
+ # リクエストの処理が終わったら、ContextVarをクリアします
815
+ request_scope.set(None)
816
+
817
+ def get_request_scope() -> Dict[str, Any]:
818
+ """
819
+ FastAPIのDepends用に、ContextVarからリクエストスコープを取得します。
820
+
821
+ Example:
822
+
823
+ ::
824
+
825
+ from cmdbox.app.auth import signin
826
+ from fastapi import Request, Response
827
+ scope = signin.get_request_scope()
828
+ scope['req'] # Requestオブジェクト
829
+ scope['res'] # Responseオブジェクト
830
+ scope['session'] # sessionを表す辞書
831
+ scope['websocket'] # WebSocket接続
832
+ scope['logger'] # loggerオブジェクト
833
+
834
+ Returns:
835
+ Dict[str, Any]: リクエストとレスポンスとWebSocket接続
836
+ """
837
+ return request_scope.get() if request_scope.get() is not None else dict(req=None, res=None, session=None, websocket=None)