cmdbox 0.5.1.2__py3-none-any.whl → 0.5.3__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 (143) hide show
  1. cmdbox/app/app.py +4 -2
  2. cmdbox/app/auth/signin.py +634 -631
  3. cmdbox/app/client.py +10 -10
  4. cmdbox/app/common.py +50 -6
  5. cmdbox/app/commons/convert.py +9 -0
  6. cmdbox/app/commons/module.py +113 -113
  7. cmdbox/app/commons/redis_client.py +40 -29
  8. cmdbox/app/edge.py +4 -4
  9. cmdbox/app/features/cli/audit_base.py +138 -0
  10. cmdbox/app/features/cli/cmdbox_audit_createdb.py +224 -0
  11. cmdbox/app/features/cli/cmdbox_audit_delete.py +308 -0
  12. cmdbox/app/features/cli/cmdbox_audit_search.py +416 -0
  13. cmdbox/app/features/cli/cmdbox_audit_write.py +247 -0
  14. cmdbox/app/features/cli/cmdbox_client_file_copy.py +207 -207
  15. cmdbox/app/features/cli/cmdbox_client_file_download.py +207 -207
  16. cmdbox/app/features/cli/cmdbox_client_file_list.py +193 -193
  17. cmdbox/app/features/cli/cmdbox_client_file_mkdir.py +191 -191
  18. cmdbox/app/features/cli/cmdbox_client_file_move.py +199 -199
  19. cmdbox/app/features/cli/cmdbox_client_file_remove.py +190 -190
  20. cmdbox/app/features/cli/cmdbox_client_file_rmdir.py +190 -190
  21. cmdbox/app/features/cli/cmdbox_client_file_upload.py +212 -212
  22. cmdbox/app/features/cli/cmdbox_client_server_info.py +166 -166
  23. cmdbox/app/features/cli/cmdbox_server_list.py +88 -88
  24. cmdbox/app/features/cli/cmdbox_server_stop.py +138 -138
  25. cmdbox/app/features/web/cmdbox_web_audit.py +81 -0
  26. cmdbox/app/features/web/cmdbox_web_audit_metrics.py +72 -0
  27. cmdbox/app/features/web/cmdbox_web_del_cmd.py +2 -0
  28. cmdbox/app/features/web/cmdbox_web_del_pipe.py +1 -0
  29. cmdbox/app/features/web/cmdbox_web_do_signin.py +12 -2
  30. cmdbox/app/features/web/cmdbox_web_do_signout.py +1 -0
  31. cmdbox/app/features/web/cmdbox_web_exec_cmd.py +31 -2
  32. cmdbox/app/features/web/cmdbox_web_exec_pipe.py +1 -0
  33. cmdbox/app/features/web/cmdbox_web_filer download.py +43 -42
  34. cmdbox/app/features/web/cmdbox_web_filer.py +1 -0
  35. cmdbox/app/features/web/cmdbox_web_filer_upload.py +65 -64
  36. cmdbox/app/features/web/cmdbox_web_gui.py +166 -165
  37. cmdbox/app/features/web/cmdbox_web_load_pin.py +43 -43
  38. cmdbox/app/features/web/cmdbox_web_raw_pipe.py +87 -87
  39. cmdbox/app/features/web/cmdbox_web_save_cmd.py +1 -0
  40. cmdbox/app/features/web/cmdbox_web_save_pin.py +42 -42
  41. cmdbox/app/features/web/cmdbox_web_save_pipe.py +1 -0
  42. cmdbox/app/features/web/cmdbox_web_user_data.py +58 -0
  43. cmdbox/app/features/web/cmdbox_web_users.py +12 -0
  44. cmdbox/app/options.py +788 -601
  45. cmdbox/app/web.py +7 -1
  46. cmdbox/extensions/features.yml +23 -0
  47. cmdbox/extensions/sample_project/sample/app/features/cli/sample_client_time.py +82 -82
  48. cmdbox/extensions/sample_project/sample/app/features/cli/sample_server_time.py +145 -145
  49. cmdbox/extensions/user_list.yml +5 -0
  50. cmdbox/licenses/{LICENSE.Sphinx.8.1.3(BSD License).txt → LICENSE.Sphinx.8.2.3(UNKNOWN).txt} +1 -1
  51. cmdbox/licenses/LICENSE.argcomplete.3.6.2(Apache Software License).txt +177 -0
  52. cmdbox/licenses/{LICENSE.babel.2.16.0(BSD License).txt → LICENSE.babel.2.17.0(BSD License).txt } +1 -1
  53. cmdbox/licenses/{LICENSE.pkginfo.1.10.0(MIT License).txt → LICENSE.charset-normalizer.3.4.1(MIT License).txt } +1 -1
  54. cmdbox/licenses/LICENSE.gevent.25.4.1(MIT).txt +25 -0
  55. cmdbox/licenses/LICENSE.greenlet.3.2.0(MIT AND Python-2.0).txt +30 -0
  56. cmdbox/licenses/LICENSE.gunicorn.23.0.0(MIT License).txt +23 -0
  57. cmdbox/licenses/LICENSE.importlib_metadata.8.6.1(Apache Software License).txt +202 -0
  58. cmdbox/licenses/LICENSE.nh3.0.2.21(MIT).txt +21 -0
  59. cmdbox/licenses/{LICENSE.pillow.11.0.0(CMU License (MIT-CMU)).txt → LICENSE.pillow.11.1.0(CMU License (MIT-CMU)).txt } +27 -40
  60. cmdbox/licenses/LICENSE.pillow.11.2.1(UNKNOWN).txt +1200 -0
  61. cmdbox/licenses/LICENSE.plyer.2.1.0(MIT License).txt +19 -0
  62. cmdbox/licenses/LICENSE.prompt_toolkit.3.0.50(BSD License).txt +27 -0
  63. cmdbox/licenses/LICENSE.prompt_toolkit.3.0.51(BSD License).txt +27 -0
  64. cmdbox/licenses/LICENSE.psycopg-binary.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +165 -0
  65. cmdbox/licenses/LICENSE.psycopg-pool.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +165 -0
  66. cmdbox/licenses/LICENSE.psycopg.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +165 -0
  67. cmdbox/licenses/LICENSE.pycryptodome.3.22.0(BSD License; Public Domain).txt +61 -0
  68. cmdbox/licenses/LICENSE.pydantic.2.11.3(MIT License).txt +21 -0
  69. cmdbox/licenses/LICENSE.pydantic_core.2.33.1(MIT License).txt +21 -0
  70. cmdbox/licenses/LICENSE.pystray.0.19.5(GNU Lesser General Public License v3 (LGPLv3)).txt +674 -0
  71. cmdbox/licenses/LICENSE.questionary.2.1.0(MIT License).txt +19 -0
  72. cmdbox/licenses/LICENSE.roman-numerals-py.3.1.0(CC0 1.0 Universal (CC0 1.0) Public Domain Dedication; Zero-Clause BSD (0BSD)).txt +146 -0
  73. cmdbox/licenses/{LICENSE.six.1.16.0(MIT License).txt → LICENSE.six.1.17.0(MIT License).txt } +1 -1
  74. cmdbox/licenses/LICENSE.starlette.0.46.2(BSD License).txt +27 -0
  75. cmdbox/licenses/{LICENSE.charset-normalizer.3.4.0(MIT License).txt → LICENSE.typing-inspection.0.4.0(MIT License).txt } +2 -2
  76. cmdbox/licenses/LICENSE.typing_extensions.4.13.2(UNKNOWN).txt +279 -0
  77. cmdbox/licenses/LICENSE.tzdata.2025.2(Apache Software License).txt +15 -0
  78. cmdbox/licenses/LICENSE.urllib3.2.4.0(UNKNOWN).txt +21 -0
  79. cmdbox/licenses/LICENSE.uvicorn.0.34.1(BSD License).txt +27 -0
  80. cmdbox/licenses/LICENSE.watchfiles.1.0.5(MIT License).txt +21 -0
  81. cmdbox/licenses/files.txt +49 -38
  82. cmdbox/logconf_audit.yml +30 -0
  83. cmdbox/logconf_cmdbox.yml +30 -0
  84. cmdbox/version.py +2 -2
  85. cmdbox/web/assets/apexcharts/apexcharts.css +679 -0
  86. cmdbox/web/assets/apexcharts/apexcharts.min.js +38 -0
  87. cmdbox/web/assets/cmdbox/audit.js +340 -0
  88. cmdbox/web/assets/cmdbox/color_mode.css +520 -0
  89. cmdbox/web/assets/cmdbox/common.js +416 -24
  90. cmdbox/web/assets/cmdbox/filer_modal.js +1 -1
  91. cmdbox/web/assets/cmdbox/list_cmd.js +10 -275
  92. cmdbox/web/assets/cmdbox/list_pipe.js +3 -3
  93. cmdbox/web/assets/cmdbox/main.js +2 -2
  94. cmdbox/web/assets/cmdbox/result.js +2 -2
  95. cmdbox/web/assets/cmdbox/signin.js +2 -2
  96. cmdbox/web/assets/cmdbox/users.js +19 -20
  97. cmdbox/web/assets/cmdbox/view_raw.js +1 -1
  98. cmdbox/web/assets/cmdbox/view_result.js +11 -13
  99. cmdbox/web/assets/filer/filer.js +2 -2
  100. cmdbox/web/assets/filer/main.js +2 -2
  101. cmdbox/web/assets_license_list.txt +4 -1
  102. cmdbox/web/audit.html +268 -0
  103. cmdbox/web/filer.html +37 -12
  104. cmdbox/web/gui.html +36 -53
  105. cmdbox/web/result.html +24 -3
  106. cmdbox/web/signin.html +35 -14
  107. cmdbox/web/users.html +21 -3
  108. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/METADATA +28 -5
  109. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/RECORD +142 -103
  110. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/entry_points.txt +0 -1
  111. cmdbox/licenses/LICENSE.nh3.0.2.18(MIT).txt +0 -1
  112. /cmdbox/licenses/{LICENSE.Jinja2.3.1.4(BSD License).txt → LICENSE.Jinja2.3.1.6(BSD License).txt} +0 -0
  113. /cmdbox/licenses/{LICENSE.Pygments.2.18.0(BSD License).txt → LICENSE.Pygments.2.19.1(BSD License).txt} +0 -0
  114. /cmdbox/licenses/{LICENSE.anyio.4.6.2.post1(MIT License).txt → LICENSE.anyio.4.9.0(MIT License).txt} +0 -0
  115. /cmdbox/licenses/{LICENSE.argcomplete.3.5.1(Apache Software License).txt → LICENSE.argcomplete.3.6.1(Apache Software License).txt} +0 -0
  116. /cmdbox/licenses/{LICENSE.certifi.2024.8.30(Mozilla Public License 2.0 (MPL 2.0)).txt → LICENSE.certifi.2025.1.31(Mozilla Public License 2.0 (MPL 2.0)).txt} +0 -0
  117. /cmdbox/licenses/{LICENSE.click.8.1.7(BSD License).txt → LICENSE.click.8.1.8(BSD License).txt} +0 -0
  118. /cmdbox/licenses/{LICENSE.cryptography.43.0.3(Apache Software License; BSD License).txt → LICENSE.cryptography.44.0.2(Apache Software License; BSD License).txt} +0 -0
  119. /cmdbox/licenses/{LICENSE.fastapi.0.115.5(MIT License).txt → LICENSE.fastapi.0.115.12(MIT License).txt} +0 -0
  120. /cmdbox/licenses/{LICENSE.importlib_metadata.8.5.0(Apache Software License).txt → LICENSE.id.1.5.0(Apache Software License).txt} +0 -0
  121. /cmdbox/licenses/{LICENSE.keyring.25.5.0(MIT License).txt → LICENSE.keyring.25.6.0(MIT License).txt} +0 -0
  122. /cmdbox/licenses/{LICENSE.more-itertools.10.5.0(MIT License).txt → LICENSE.more-itertools.10.6.0(MIT License).txt} +0 -0
  123. /cmdbox/licenses/{LICENSE.numpy.2.1.3(BSD License).txt → LICENSE.numpy.2.2.4(BSD License).txt} +0 -0
  124. /cmdbox/licenses/{LICENSE.prettytable.3.12.0(BSD License).txt → LICENSE.prettytable.3.16.0(UNKNOWN).txt} +0 -0
  125. /cmdbox/licenses/{LICENSE.pydantic.2.10.2(MIT License).txt → LICENSE.pydantic.2.11.1(MIT License).txt} +0 -0
  126. /cmdbox/licenses/{LICENSE.pydantic_core.2.27.1(MIT License).txt → LICENSE.pydantic_core.2.33.0(MIT License).txt} +0 -0
  127. /cmdbox/licenses/{LICENSE.python-dotenv.1.0.1(BSD License).txt → LICENSE.python-dotenv.1.1.0(BSD License).txt} +0 -0
  128. /cmdbox/licenses/{LICENSE.python-multipart.0.0.17(Apache Software License).txt → LICENSE.python-multipart.0.0.20(Apache Software License).txt} +0 -0
  129. /cmdbox/licenses/{LICENSE.redis.5.2.0(MIT License).txt → LICENSE.redis.5.2.1(MIT License).txt} +0 -0
  130. /cmdbox/licenses/{LICENSE.rich.13.9.4(MIT License).txt → LICENSE.rich.14.0.0(MIT License).txt} +0 -0
  131. /cmdbox/licenses/{LICENSE.sphinx-intl.2.3.0(BSD License).txt → LICENSE.sphinx-intl.2.3.1(BSD License).txt} +0 -0
  132. /cmdbox/licenses/{LICENSE.starlette.0.41.3(BSD License).txt → LICENSE.starlette.0.46.1(BSD License).txt} +0 -0
  133. /cmdbox/licenses/{LICENSE.tomli.2.1.0(MIT License).txt → LICENSE.tomli.2.2.1(MIT License).txt} +0 -0
  134. /cmdbox/licenses/{LICENSE.twine.5.1.1(Apache Software License).txt → LICENSE.twine.6.1.0(Apache Software License).txt} +0 -0
  135. /cmdbox/licenses/{LICENSE.typing_extensions.4.12.2(Python Software Foundation License).txt → LICENSE.typing_extensions.4.13.0(UNKNOWN).txt} +0 -0
  136. /cmdbox/licenses/{LICENSE.urllib3.2.2.3(MIT License).txt → LICENSE.urllib3.2.3.0(MIT License).txt} +0 -0
  137. /cmdbox/licenses/{LICENSE.uvicorn.0.32.1(BSD License).txt → LICENSE.uvicorn.0.34.0(BSD License).txt} +0 -0
  138. /cmdbox/licenses/{LICENSE.watchfiles.1.0.0(MIT License).txt → LICENSE.watchfiles.1.0.4(MIT License).txt} +0 -0
  139. /cmdbox/licenses/{LICENSE.websockets.14.1(BSD License).txt → LICENSE.websockets.15.0.1(BSD License).txt} +0 -0
  140. /cmdbox/licenses/{LICENSE.zope.interface.7.1.1(Zope Public License).txt → LICENSE.zope.interface.7.2(Zope Public License).txt} +0 -0
  141. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/LICENSE +0 -0
  142. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/WHEEL +0 -0
  143. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/top_level.txt +0 -0
cmdbox/app/auth/signin.py CHANGED
@@ -1,631 +1,634 @@
1
- from cmdbox.app import common, options
2
- from fastapi import Request, Response, HTTPException
3
- from fastapi.responses import RedirectResponse
4
- from pathlib import Path
5
- from typing import Dict, Any, Tuple, List, Union
6
- import copy
7
- import logging
8
-
9
-
10
- class Signin(object):
11
-
12
- def __init__(self, logger:logging.Logger, signin_file:Path, signin_file_data:Dict[str, Any], appcls, ver):
13
- self.logger = logger
14
- self.signin_file = signin_file
15
- self.signin_file_data = signin_file_data
16
- self.options = options.Options.getInstance(appcls, ver)
17
- self.ver = ver
18
- self.appcls = appcls
19
-
20
- def get_data(self) -> Dict[str, Any]:
21
- """
22
- サインインデータを返します
23
-
24
- Returns:
25
- Dict[str, Any]: サインインデータ
26
- """
27
- return self.signin_file_data
28
-
29
- def jadge(self, access_token:str, email:str) -> Tuple[bool, Dict[str, Any]]:
30
- """
31
- サインインを成功させるかどうかを判定します。
32
- 返すユーザーデータには、uid, name, email, groups, hash が必要です。
33
-
34
- Args:
35
- access_token (str): アクセストークン
36
- email (str): メールアドレス
37
-
38
- Returns:
39
- Tuple[bool, Dict[str, Any]]: (成功かどうか, ユーザーデータ)
40
- """
41
- copy_signin_data = copy.deepcopy(self.signin_file_data)
42
- users = [u for u in copy_signin_data['users'] if u['email'] == email and u['hash'] == 'oauth2']
43
- return len(users) > 0, users[0] if len(users) > 0 else None
44
-
45
- def get_groups(self, access_token:str, user:Dict[str, Any]) -> Tuple[List[str], List[int]]:
46
- """
47
- ユーザーのグループを取得します
48
-
49
- Args:
50
- access_token (str): アクセストークン
51
- user (Dict[str, Any]): ユーザーデータ
52
- signin_file_data (Dict[str, Any]): サインインファイルデータ(変更不可)
53
-
54
- Returns:
55
- Tuple[List[str], List[int]]: (グループ名, グループID)
56
- """
57
- copy_signin_data = copy.deepcopy(self.signin_file_data)
58
- group_names = list(set(self.__class__.correct_group(copy_signin_data, user['groups'], None)))
59
- gids = [g['gid'] for g in copy_signin_data['groups'] if g['name'] in group_names]
60
- return group_names, gids
61
-
62
- def enable_cors(self, req:Request, res:Response) -> None:
63
- """
64
- CORSを有効にする
65
-
66
- Args:
67
- req (Request): リクエスト
68
- res (Response): レスポンス
69
- """
70
- if req is None or not 'Origin' in req.headers.keys():
71
- return
72
- res.headers['Access-Control-Allow-Origin'] = res.headers['Origin']
73
-
74
- def check_signin(self, req:Request, res:Response):
75
- """
76
- サインインをチェックする
77
-
78
- Args:
79
- req (Request): リクエスト
80
- res (Response): レスポンス
81
-
82
- Returns:
83
- Response: サインインエラーの場合はリダイレクトレスポンス
84
- """
85
- self.enable_cors(req, res)
86
- if self.signin_file_data is None:
87
- return None
88
- if 'signin' in req.session:
89
- self.signin_file_data = self.load_signin_file(self.signin_file, self.signin_file_data) # サインインファイルの更新をチェック
90
- path_jadge = self.check_path(req, req.url.path)
91
- if path_jadge is not None:
92
- return path_jadge
93
- return None
94
- self.logger.info(f"Not found siginin session. Try check_apikey. path={req.url.path}")
95
- ret = self.check_apikey(req, res)
96
- if ret is not None and self.logger.level == logging.DEBUG:
97
- self.logger.debug(f"Not signed in.")
98
- return ret
99
-
100
- def check_apikey(self, req:Request, res:Response):
101
- """
102
- ApiKeyをチェックする
103
-
104
- Args:
105
- req (Request): リクエスト
106
- res (Response): レスポンス
107
-
108
- Returns:
109
- Response: サインインエラーの場合はリダイレクトレスポンス
110
- """
111
- self.enable_cors(req, res)
112
- if self.signin_file_data is None:
113
- res.headers['signin'] = 'success'
114
- return None
115
- if 'Authorization' not in req.headers:
116
- self.logger.warning(f"Authorization not found. headers={req.headers}")
117
- return RedirectResponse(url=f'/signin{req.url.path}?error=noauth')
118
- auth = req.headers['Authorization']
119
- if not auth.startswith('Bearer '):
120
- self.logger.warning(f"Bearer not found. headers={req.headers}")
121
- return RedirectResponse(url=f'/signin{req.url.path}?error=apikeyfail')
122
- bearer, apikey = auth.split(' ')
123
- apikey = common.hash_password(apikey.strip(), 'sha1')
124
- if self.logger.level == logging.DEBUG:
125
- self.logger.debug(f"hashed apikey: {apikey}")
126
- find_user = None
127
- self.signin_file_data = self.load_signin_file(self.signin_file, self.signin_file_data) # サインインファイルの更新をチェック
128
- for user in self.signin_file_data['users']:
129
- if 'apikeys' not in user:
130
- continue
131
- for ak, key in user['apikeys'].items():
132
- if apikey == key:
133
- find_user = user
134
- if find_user is None:
135
- self.logger.warning(f"No matching user found for apikey.")
136
- return RedirectResponse(url=f'/signin{req.url.path}?error=apikeyfail')
137
-
138
- group_names = list(set(self.__class__.correct_group(self.get_data(), find_user['groups'], None)))
139
- gids = [g['gid'] for g in self.signin_file_data['groups'] if g['name'] in group_names]
140
- req.session['signin'] = dict(uid=find_user['uid'], name=find_user['name'], password=find_user['password'],
141
- gids=gids, groups=group_names)
142
- if self.logger.level == logging.DEBUG:
143
- self.logger.debug(f"find user: name={find_user['name']}, group_names={group_names}")
144
- # パスルールチェック
145
- user_groups = find_user['groups']
146
- jadge = self.signin_file_data['pathrule']['policy']
147
- for rule in self.signin_file_data['pathrule']['rules']:
148
- if len([g for g in rule['groups'] if g in user_groups]) <= 0:
149
- continue
150
- if len([p for p in rule['paths'] if req.url.path.startswith(p)]) <= 0:
151
- continue
152
- jadge = rule['rule']
153
- if self.logger.level == logging.DEBUG:
154
- self.logger.debug(f"rule: {req.url.path}: {jadge}")
155
- if jadge == 'allow':
156
- res.headers['signin'] = 'success'
157
- return None
158
- self.logger.warning(f"Unauthorized site. user={find_user['name']}, path={req.url.path}")
159
- return RedirectResponse(url=f'/signin{req.url.path}?error=unauthorizedsite')
160
-
161
- @classmethod
162
- def load_signin_file(cls, signin_file:Path, signin_file_data:Dict[str, Any]=None) -> Dict[str, Any]:
163
- """
164
- サインインファイルを読み込む
165
-
166
- Args:
167
- signin_file (Path): サインインファイル
168
- signin_file_data (Dict[str, Any]): サインインファイルデータ
169
-
170
- Raises:
171
- HTTPException: サインインファイルのフォーマットエラー
172
-
173
- Returns:
174
- Dict[str, Any]: サインインファイルデータ
175
- """
176
- if signin_file is not None:
177
- if not signin_file.is_file():
178
- raise HTTPException(status_code=500, detail=f'signin_file is not found. ({signin_file})')
179
- # サインインファイル読込み済みなら返すが、別プロセスがサインインファイルを更新していたら読込みを実施する。
180
- if not hasattr(cls, 'signin_file_last'):
181
- cls.signin_file_last = signin_file.stat().st_mtime
182
- if cls.signin_file_last >= signin_file.stat().st_mtime and signin_file_data is not None:
183
- return signin_file_data
184
- cls.signin_file_last = signin_file.stat().st_mtime
185
- yml = common.load_yml(signin_file)
186
- # usersのフォーマットチェック
187
- if 'users' not in yml:
188
- raise HTTPException(status_code=500, detail=f'signin_file format error. "users" not found. ({signin_file})')
189
- uids = set()
190
- unames = set()
191
- groups = [g['name'] for g in yml['groups']]
192
- for user in yml['users']:
193
- if 'uid' not in user or user['uid'] is None:
194
- raise HTTPException(status_code=500, detail=f'signin_file format error. "uid" not found or empty. ({signin_file})')
195
- if user['uid'] in uids:
196
- raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate uid found. ({signin_file}). uid={user["uid"]}')
197
- if 'name' not in user or user['name'] is None:
198
- raise HTTPException(status_code=500, detail=f'signin_file format error. "name" not found or empty. ({signin_file})')
199
- if user['name'] in unames:
200
- raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate name found. ({signin_file}). name={user["name"]}')
201
- if 'password' not in user:
202
- raise HTTPException(status_code=500, detail=f'signin_file format error. "password" not found or empty. ({signin_file})')
203
- if 'hash' not in user or user['hash'] is None:
204
- raise HTTPException(status_code=500, detail=f'signin_file format error. "hash" not found or empty. ({signin_file})')
205
- if user['hash'] not in ['oauth2', 'plain', 'md5', 'sha1', 'sha256']:
206
- 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.')
207
- if 'groups' not in user or type(user['groups']) is not list:
208
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found or not list type. ({signin_file})')
209
- if len([ug for ug in user['groups'] if ug not in groups]) > 0:
210
- raise HTTPException(status_code=500, detail=f'signin_file format error. Group not found. ({signin_file}). {user["groups"]}')
211
- uids.add(user['uid'])
212
- unames.add(user['name'])
213
- # groupsのフォーマットチェック
214
- if 'groups' not in yml:
215
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found. ({signin_file})')
216
- gids = set()
217
- gnames = set()
218
- for group in yml['groups']:
219
- if 'gid' not in group or group['gid'] is None:
220
- raise HTTPException(status_code=500, detail=f'signin_file format error. "gid" not found or empty. ({signin_file})')
221
- if group['gid'] in gids:
222
- raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate gid found. ({signin_file}). gid={group["gid"]}')
223
- if 'name' not in group or group['name'] is None:
224
- raise HTTPException(status_code=500, detail=f'signin_file format error. "name" not found or empty. ({signin_file})')
225
- if group['name'] in gnames:
226
- raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate name found. ({signin_file}). name={group["name"]}')
227
- if 'parent' in group:
228
- if group['parent'] not in groups:
229
- raise HTTPException(status_code=500, detail=f'signin_file format error. Parent group not found. ({signin_file}). parent={group["parent"]}')
230
- gids.add(group['gid'])
231
- gnames.add(group['name'])
232
- # cmdruleのフォーマットチェック
233
- if 'cmdrule' not in yml:
234
- raise HTTPException(status_code=500, detail=f'signin_file format error. "cmdrule" not found. ({signin_file})')
235
- if 'policy' not in yml['cmdrule']:
236
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "cmdrule". ({signin_file})')
237
- if yml['cmdrule']['policy'] not in ['allow', 'deny']:
238
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not supported in "cmdrule". ({signin_file}). "allow" or "deny" only.')
239
- if 'rules' not in yml['cmdrule']:
240
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not found in "cmdrule". ({signin_file})')
241
- if type(yml['cmdrule']['rules']) is not list:
242
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not list type in "cmdrule". ({signin_file})')
243
- for rule in yml['cmdrule']['rules']:
244
- if 'groups' not in rule:
245
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found in "cmdrule.rules" ({signin_file})')
246
- if type(rule['groups']) is not list:
247
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not list type in "cmdrule.rules". ({signin_file})')
248
- rule['groups'] = list(set(copy.deepcopy(cls.correct_group(yml, rule['groups'], yml['groups']))))
249
- if 'rule' not in rule:
250
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not found in "cmdrule.rules" ({signin_file})')
251
- if rule['rule'] not in ['allow', 'deny']:
252
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not supported in "cmdrule.rules". ({signin_file}). "allow" or "deny" only.')
253
- if 'mode' not in rule:
254
- rule['mode'] = None
255
- if 'cmds' not in rule:
256
- rule['cmds'] = []
257
- if rule['mode'] is not None and len(rule['cmds']) <= 0:
258
- raise HTTPException(status_code=500, detail=f'signin_file format error. When “cmds” is specified, “mode” must be specified. ({signin_file})')
259
- if type(rule['cmds']) is not list:
260
- raise HTTPException(status_code=500, detail=f'signin_file format error. "cmds" not list type in "cmdrule.rules". ({signin_file})')
261
- # pathruleのフォーマットチェック
262
- if 'pathrule' not in yml:
263
- raise HTTPException(status_code=500, detail=f'signin_file format error. "pathrule" not found. ({signin_file})')
264
- if 'policy' not in yml['pathrule']:
265
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "pathrule". ({signin_file})')
266
- if yml['pathrule']['policy'] not in ['allow', 'deny']:
267
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not supported in "pathrule". ({signin_file}). "allow" or "deny" only.')
268
- if 'rules' not in yml['pathrule']:
269
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not found in "pathrule". ({signin_file})')
270
- if type(yml['pathrule']['rules']) is not list:
271
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not list type in "pathrule". ({signin_file})')
272
- for rule in yml['pathrule']['rules']:
273
- if 'groups' not in rule:
274
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found in "pathrule.rules" ({signin_file})')
275
- if type(rule['groups']) is not list:
276
- raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not list type in "pathrule.rules". ({signin_file})')
277
- rule['groups'] = list(set(copy.deepcopy(cls.correct_group(yml, rule['groups'], yml['groups']))))
278
- if 'rule' not in rule:
279
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not found in "pathrule.rules" ({signin_file})')
280
- if rule['rule'] not in ['allow', 'deny']:
281
- raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not supported in "pathrule.rules". ({signin_file}). "allow" or "deny" only.')
282
- if 'paths' not in rule:
283
- rule['paths'] = []
284
- if type(rule['paths']) is not list:
285
- raise HTTPException(status_code=500, detail=f'signin_file format error. "paths" not list type in "pathrule.rules". ({signin_file})')
286
- # passwordのフォーマットチェック
287
- if 'password' in yml:
288
- if 'policy' not in yml['password']:
289
- raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "password". ({signin_file})')
290
- if 'enabled' not in yml['password']['policy']:
291
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.policy". ({signin_file})')
292
- if type(yml['password']['policy']['enabled']) is not bool:
293
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.policy". ({signin_file})')
294
- if type(yml['password']['policy']['not_same_before']) is not bool:
295
- raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not bool type in "password.policy". ({signin_file})')
296
- if 'min_length' not in yml['password']['policy']:
297
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not found in "password.policy". ({signin_file})')
298
- if type(yml['password']['policy']['min_length']) is not int:
299
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not int type in "password.policy". ({signin_file})')
300
- if 'max_length' not in yml['password']['policy']:
301
- raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not found in "password.policy". ({signin_file})')
302
- if type(yml['password']['policy']['max_length']) is not int:
303
- raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not int type in "password.policy". ({signin_file})')
304
- if 'min_lowercase' not in yml['password']['policy']:
305
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not found in "password.policy". ({signin_file})')
306
- if type(yml['password']['policy']['min_lowercase']) is not int:
307
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not int type in "password.policy". ({signin_file})')
308
- if 'min_uppercase' not in yml['password']['policy']:
309
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not found in "password.policy". ({signin_file})')
310
- if type(yml['password']['policy']['min_uppercase']) is not int:
311
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not int type in "password.policy". ({signin_file})')
312
- if 'min_digit' not in yml['password']['policy']:
313
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not found in "password.policy". ({signin_file})')
314
- if type(yml['password']['policy']['min_digit']) is not int:
315
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not int type in "password.policy". ({signin_file})')
316
- if 'min_symbol' not in yml['password']['policy']:
317
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not found in "password.policy". ({signin_file})')
318
- if type(yml['password']['policy']['min_symbol']) is not int:
319
- raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not int type in "password.policy". ({signin_file})')
320
- if 'not_contain_username' not in yml['password']['policy']:
321
- raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not found in "password.policy". ({signin_file})')
322
- if type(yml['password']['policy']['not_contain_username']) is not bool:
323
- raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not bool type in "password.policy". ({signin_file})')
324
- if 'expiration' not in yml['password']:
325
- raise HTTPException(status_code=500, detail=f'signin_file format error. "expiration" not found in "password". ({signin_file})')
326
- if 'enabled' not in yml['password']['expiration']:
327
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.expiration". ({signin_file})')
328
- if type(yml['password']['expiration']['enabled']) is not bool:
329
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.expiration". ({signin_file})')
330
- if 'period' not in yml['password']['expiration']:
331
- raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not found in "password.expiration". ({signin_file})')
332
- if type(yml['password']['expiration']['period']) is not int:
333
- raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not int type in "password.expiration". ({signin_file})')
334
- if 'notify' not in yml['password']['expiration']:
335
- raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not found in "password.expiration". ({signin_file})')
336
- if type(yml['password']['expiration']['notify']) is not int:
337
- raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not int type in "password.expiration". ({signin_file})')
338
- if 'lockout' not in yml['password']:
339
- raise HTTPException(status_code=500, detail=f'signin_file format error. "lockout" not found in "password". ({signin_file})')
340
- if 'enabled' not in yml['password']['lockout']:
341
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.lockout". ({signin_file})')
342
- if type(yml['password']['lockout']['enabled']) is not bool:
343
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.lockout". ({signin_file})')
344
- if 'threshold' not in yml['password']['lockout']:
345
- raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not found in "password.lockout". ({signin_file})')
346
- if type(yml['password']['lockout']['threshold']) is not int:
347
- raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not int type in "password.lockout". ({signin_file})')
348
- if 'reset' not in yml['password']['lockout']:
349
- raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not found in "password.lockout". ({signin_file})')
350
- if type(yml['password']['lockout']['reset']) is not int:
351
- raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not int type in "password.lockout". ({signin_file})')
352
- # oauth2のフォーマットチェック
353
- if 'oauth2' not in yml:
354
- raise HTTPException(status_code=500, detail=f'signin_file format error. "oauth2" not found. ({signin_file})')
355
- if 'providers' not in yml['oauth2']:
356
- raise HTTPException(status_code=500, detail=f'signin_file format error. "providers" not found in "oauth2". ({signin_file})')
357
- # google
358
- if 'google' not in yml['oauth2']['providers']:
359
- raise HTTPException(status_code=500, detail=f'signin_file format error. "google" not found in "providers". ({signin_file})')
360
- if 'enabled' not in yml['oauth2']['providers']['google']:
361
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "google". ({signin_file})')
362
- if type(yml['oauth2']['providers']['google']['enabled']) is not bool:
363
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "google". ({signin_file})')
364
- if 'client_id' not in yml['oauth2']['providers']['google']:
365
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "google". ({signin_file})')
366
- if 'client_secret' not in yml['oauth2']['providers']['google']:
367
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "google". ({signin_file})')
368
- if 'redirect_uri' not in yml['oauth2']['providers']['google']:
369
- raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "google". ({signin_file})')
370
- if 'scope' not in yml['oauth2']['providers']['google']:
371
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "google". ({signin_file})')
372
- if type(yml['oauth2']['providers']['google']['scope']) is not list:
373
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "google". ({signin_file})')
374
- if 'signin_module' not in yml['oauth2']['providers']['google']:
375
- raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "google". ({signin_file})')
376
- # github
377
- if 'github' not in yml['oauth2']['providers']:
378
- raise HTTPException(status_code=500, detail=f'signin_file format error. "github" not found in "providers". ({signin_file})')
379
- if 'enabled' not in yml['oauth2']['providers']['github']:
380
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "github". ({signin_file})')
381
- if type(yml['oauth2']['providers']['github']['enabled']) is not bool:
382
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "github". ({signin_file})')
383
- if 'client_id' not in yml['oauth2']['providers']['github']:
384
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "github". ({signin_file})')
385
- if 'client_secret' not in yml['oauth2']['providers']['github']:
386
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "github". ({signin_file})')
387
- if 'redirect_uri' not in yml['oauth2']['providers']['github']:
388
- raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "github". ({signin_file})')
389
- if 'scope' not in yml['oauth2']['providers']['github']:
390
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "github". ({signin_file})')
391
- if type(yml['oauth2']['providers']['github']['scope']) is not list:
392
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "github". ({signin_file})')
393
- if 'signin_module' not in yml['oauth2']['providers']['github']:
394
- raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "github". ({signin_file})')
395
- # azure
396
- if 'azure' not in yml['oauth2']['providers']:
397
- raise HTTPException(status_code=500, detail=f'signin_file format error. "azure" not found in "providers". ({signin_file})')
398
- if 'enabled' not in yml['oauth2']['providers']['azure']:
399
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "azure". ({signin_file})')
400
- if type(yml['oauth2']['providers']['azure']['enabled']) is not bool:
401
- raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "azure". ({signin_file})')
402
- if 'tenant_id' not in yml['oauth2']['providers']['azure']:
403
- raise HTTPException(status_code=500, detail=f'signin_file format error. "tenant_id" not found in "azure". ({signin_file})')
404
- if 'client_id' not in yml['oauth2']['providers']['azure']:
405
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "azure". ({signin_file})')
406
- if 'client_secret' not in yml['oauth2']['providers']['azure']:
407
- raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "azure". ({signin_file})')
408
- if 'redirect_uri' not in yml['oauth2']['providers']['azure']:
409
- raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "azure". ({signin_file})')
410
- if 'scope' not in yml['oauth2']['providers']['azure']:
411
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "azure". ({signin_file})')
412
- if type(yml['oauth2']['providers']['azure']['scope']) is not list:
413
- raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "azure". ({signin_file})')
414
- if 'signin_module' not in yml['oauth2']['providers']['azure']:
415
- raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "azure". ({signin_file})')
416
- # フォーマットチェックOK
417
- return yml
418
-
419
- @classmethod
420
- def correct_group(cls, signin_file_data:Dict[str, Any], group_names:List[str], master_groups:List[Dict[str, Any]]) -> List[str]:
421
- """
422
- 指定されたグループ名に属する子グループ名を収集します
423
-
424
- Args:
425
- signin_file_data (Dict[str, Any]): サインインファイルデータ
426
- group_names (List[str]): グループ名リスト
427
- master_groups (List[Dict[str, Any]], optional): 親グループ名. Defaults to None.
428
- """
429
- copy_signin_data = copy.deepcopy(signin_file_data)
430
- master_groups = copy_signin_data['groups'] if master_groups is None else master_groups
431
- gns = []
432
- for gn in group_names.copy():
433
- gns = [gr['name'] for gr in master_groups if 'parent' in gr and gr['parent']==gn]
434
- gns += cls.correct_group(copy_signin_data, gns, master_groups)
435
- return group_names + gns
436
-
437
- def check_path(self, req:Request, path:str) -> Union[None, RedirectResponse]:
438
- """
439
- パスの認可をチェックします
440
-
441
- Args:
442
- req (Request): リクエスト
443
- path (str): パス
444
-
445
- Returns:
446
- Union[None, RedirectResponse]: 認可された場合はNone、認可されなかった場合はリダイレクトレスポンス
447
- """
448
- if self.signin_file_data is None:
449
- return None
450
- if 'signin' not in req.session:
451
- return None
452
- path = path if path.startswith('/') else f'/{path}'
453
- # パスルールチェック
454
- user_groups = req.session['signin']['groups']
455
- jadge = self.signin_file_data['pathrule']['policy']
456
- for rule in self.signin_file_data['pathrule']['rules']:
457
- if len([g for g in rule['groups'] if g in user_groups]) <= 0:
458
- continue
459
- if len([p for p in rule['paths'] if path.startswith(p)]) <= 0:
460
- continue
461
- jadge = rule['rule']
462
- if self.logger.level == logging.DEBUG:
463
- self.logger.debug(f"rule: {path}: {jadge}")
464
- if jadge == 'allow':
465
- return None
466
- else:
467
- self.logger.warning(f"Unauthorized site. user={req.session['signin']['name']}, path={path}")
468
- return RedirectResponse(url=f'/signin{path}?error=unauthorizedsite')
469
-
470
- def check_cmd(self, req:Request, res:Response, mode:str, cmd:str):
471
- """
472
- コマンドの認可をチェックします
473
-
474
- Args:
475
- req (Request): リクエスト
476
- res (Response): レスポンス
477
- mode (str): モード
478
- cmd (str): コマンド
479
-
480
- Returns:
481
- bool: 認可されたかどうか
482
- """
483
- if self.signin_file_data is None:
484
- return True
485
- if 'signin' not in req.session or 'groups' not in req.session['signin']:
486
- return False
487
- # コマンドチェック
488
- user_groups = req.session['signin']['groups']
489
- jadge = self.signin_file_data['cmdrule']['policy']
490
- for rule in self.signin_file_data['cmdrule']['rules']:
491
- if len([g for g in rule['groups'] if g in user_groups]) <= 0:
492
- continue
493
- if rule['mode'] is not None:
494
- if rule['mode'] != mode:
495
- continue
496
- if len([c for c in rule['cmds'] if cmd == c]) <= 0:
497
- continue
498
- jadge = rule['rule']
499
- if self.logger.level == logging.DEBUG:
500
- self.logger.debug(f"rule: mode={mode}, cmd={cmd}: {jadge}")
501
- return jadge == 'allow'
502
-
503
- def get_enable_modes(self, req:Request, res:Response) -> List[str]:
504
- """
505
- 認可されたモードを取得します
506
-
507
- Args:
508
- req (Request): リクエスト
509
- res (Response): レスポンス
510
-
511
- Returns:
512
- List[str]: 認可されたモード
513
- """
514
- if self.signin_file_data is None:
515
- return self.options.get_modes().copy()
516
- if 'signin' not in req.session or 'groups' not in req.session['signin']:
517
- return []
518
- modes = self.options.get_modes().copy()
519
- user_groups = req.session['signin']['groups']
520
- jadge = self.signin_file_data['cmdrule']['policy']
521
- jadge_modes = []
522
- if jadge == 'allow':
523
- for m in modes:
524
- jadge_modes += list(m.keys()) if type(m) is dict else [m]
525
- for rule in self.signin_file_data['cmdrule']['rules']:
526
- if len([g for g in rule['groups'] if g in user_groups]) <= 0:
527
- continue
528
- if 'mode' not in rule:
529
- continue
530
- if rule['mode'] is not None:
531
- if rule['rule'] == 'allow':
532
- jadge_modes.append(rule['mode'])
533
- elif rule['rule'] == 'deny':
534
- jadge_modes.remove(rule['mode'])
535
- elif rule['mode'] is None and len(rule['cmds']) <= 0:
536
- if rule['rule'] == 'allow':
537
- for m in modes:
538
- jadge_modes += list(m.keys()) if type(m) is dict else [m]
539
- elif rule['rule'] == 'deny':
540
- jadge_modes = []
541
- return sorted(list(set(['']+jadge_modes)), key=lambda m: m)
542
-
543
- def get_enable_cmds(self, mode:str, req:Request, res:Response) -> List[str]:
544
- """
545
- 認可されたコマンドを取得します
546
-
547
- Args:
548
- mode (str): モード
549
- req (Request): リクエスト
550
- res (Response): レスポンス
551
-
552
- Returns:
553
- List[str]: 認可されたコマンド
554
- """
555
- if self.signin_file_data is None:
556
- cmds = self.options.get_cmds(mode).copy()
557
- return cmds
558
- if 'signin' not in req.session or 'groups' not in req.session['signin']:
559
- return []
560
- cmds = self.options.get_cmds(mode).copy()
561
- if mode == '':
562
- return cmds
563
- user_groups = req.session['signin']['groups']
564
- jadge = self.signin_file_data['cmdrule']['policy']
565
- jadge_cmds = []
566
- if jadge == 'allow':
567
- for c in cmds:
568
- jadge_cmds += list(c.keys()) if type(c) is dict else [c]
569
- for rule in self.signin_file_data['cmdrule']['rules']:
570
- if len([g for g in rule['groups'] if g in user_groups]) <= 0:
571
- continue
572
- if 'mode' not in rule:
573
- continue
574
- if 'cmds' not in rule:
575
- continue
576
- if rule['mode'] is not None and rule['mode'] != mode:
577
- continue
578
- if len(rule['cmds']) > 0:
579
- if rule['rule'] == 'allow':
580
- jadge_cmds += rule['cmds']
581
- elif rule['rule'] == 'deny':
582
- for c in rule['cmds']:
583
- jadge_cmds.remove[c]
584
- elif rule['mode'] is None and len(rule['cmds']) <= 0:
585
- if rule['rule'] == 'allow':
586
- for c in cmds:
587
- jadge_cmds += list(c.keys()) if type(c) is dict else [c]
588
- elif rule['rule'] == 'deny':
589
- jadge_cmds = []
590
- return sorted(list(set(['']+jadge_cmds)), key=lambda c: c)
591
-
592
- def check_password_policy(self, user_name:str, password:str, new_password:str) -> Tuple[bool, str]:
593
- """
594
- パスワードポリシーをチェックする
595
-
596
- Args:
597
- user_name (str): ユーザー名
598
- password (str): 元パスワード
599
- new_password (str): 新しいパスワード
600
- Returns:
601
- bool: True:ポリシーOK, False:ポリシーNG
602
- str: メッセージ
603
- """
604
- if self.signin_file_data is None or 'password' not in self.signin_file_data:
605
- return True, "There is no password policy set."
606
- policy = self.signin_file_data['password']['policy']
607
- if not policy['enabled']:
608
- return True, "Password policy is disabled."
609
- if policy['not_same_before'] and password == new_password:
610
- self.logger.warning(f"Password policy error. The same password cannot be changed.")
611
- return False, f"Password policy error. The same password cannot be changed."
612
- if len(new_password) < policy['min_length'] or len(new_password) > policy['max_length']:
613
- self.logger.warning(f"Password policy error. min_length={policy['min_length']}, max_length={policy['max_length']}")
614
- return False, f"Password policy error. min_length={policy['min_length']}, max_length={policy['max_length']}"
615
- if len([c for c in new_password if c.islower()]) < policy['min_lowercase']:
616
- self.logger.warning(f"Password policy error. min_lowercase={policy['min_lowercase']}")
617
- return False, f"Password policy error. min_lowercase={policy['min_lowercase']}"
618
- if len([c for c in new_password if c.isupper()]) < policy['min_uppercase']:
619
- self.logger.warning(f"Password policy error. min_uppercase={policy['min_uppercase']}")
620
- return False, f"Password policy error. min_uppercase={policy['min_uppercase']}"
621
- if len([c for c in new_password if c.isdigit()]) < policy['min_digit']:
622
- self.logger.warning(f"Password policy error. min_digit={policy['min_digit']}")
623
- return False, f"Password policy error. min_digit={policy['min_digit']}"
624
- if len([c for c in new_password if c in string.punctuation]) < policy['min_symbol']:
625
- self.logger.warning(f"Password policy error. min_symbol={policy['min_symbol']}")
626
- return False, f"Password policy error. min_symbol={policy['min_symbol']}"
627
- if policy['not_contain_username'] and (user_name is None or user_name in new_password):
628
- self.logger.warning(f"Password policy error. not_contain_username=True")
629
- return False, f"Password policy error. not_contain_username=True"
630
- self.logger.info(f"Password policy OK.")
631
- return True, "Password policy OK."
1
+ from cmdbox.app import common, options
2
+ from fastapi import Request, Response, HTTPException
3
+ from fastapi.responses import RedirectResponse
4
+ from pathlib import Path
5
+ from typing import Dict, Any, Tuple, List, Union
6
+ import copy
7
+ import logging
8
+ import string
9
+
10
+
11
+ class Signin(object):
12
+
13
+ def __init__(self, logger:logging.Logger, signin_file:Path, signin_file_data:Dict[str, Any], appcls, ver):
14
+ self.logger = logger
15
+ self.signin_file = signin_file
16
+ self.signin_file_data = signin_file_data
17
+ self.options = options.Options.getInstance(appcls, ver)
18
+ self.ver = ver
19
+ self.appcls = appcls
20
+
21
+ def get_data(self) -> Dict[str, Any]:
22
+ """
23
+ サインインデータを返します
24
+
25
+ Returns:
26
+ Dict[str, Any]: サインインデータ
27
+ """
28
+ return self.signin_file_data
29
+
30
+ def jadge(self, access_token:str, email:str) -> Tuple[bool, Dict[str, Any]]:
31
+ """
32
+ サインインを成功させるかどうかを判定します。
33
+ 返すユーザーデータには、uid, name, email, groups, hash が必要です。
34
+
35
+ Args:
36
+ access_token (str): アクセストークン
37
+ email (str): メールアドレス
38
+
39
+ Returns:
40
+ Tuple[bool, Dict[str, Any]]: (成功かどうか, ユーザーデータ)
41
+ """
42
+ copy_signin_data = copy.deepcopy(self.signin_file_data)
43
+ users = [u for u in copy_signin_data['users'] if u['email'] == email and u['hash'] == 'oauth2']
44
+ return len(users) > 0, users[0] if len(users) > 0 else None
45
+
46
+ def get_groups(self, access_token:str, user:Dict[str, Any]) -> Tuple[List[str], List[int]]:
47
+ """
48
+ ユーザーのグループを取得します
49
+
50
+ Args:
51
+ access_token (str): アクセストークン
52
+ user (Dict[str, Any]): ユーザーデータ
53
+ signin_file_data (Dict[str, Any]): サインインファイルデータ(変更不可)
54
+
55
+ Returns:
56
+ Tuple[List[str], List[int]]: (グループ名, グループID)
57
+ """
58
+ copy_signin_data = copy.deepcopy(self.signin_file_data)
59
+ group_names = list(set(self.__class__.correct_group(copy_signin_data, user['groups'], None)))
60
+ gids = [g['gid'] for g in copy_signin_data['groups'] if g['name'] in group_names]
61
+ return group_names, gids
62
+
63
+ def enable_cors(self, req:Request, res:Response) -> None:
64
+ """
65
+ CORSを有効にする
66
+
67
+ Args:
68
+ req (Request): リクエスト
69
+ res (Response): レスポンス
70
+ """
71
+ if req is None or not 'Origin' in req.headers.keys():
72
+ return
73
+ res.headers['Access-Control-Allow-Origin'] = res.headers['Origin']
74
+
75
+ def check_signin(self, req:Request, res:Response):
76
+ """
77
+ サインインをチェックする
78
+
79
+ Args:
80
+ req (Request): リクエスト
81
+ res (Response): レスポンス
82
+
83
+ Returns:
84
+ Response: サインインエラーの場合はリダイレクトレスポンス
85
+ """
86
+ self.enable_cors(req, res)
87
+ if self.signin_file_data is None:
88
+ return None
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)
92
+ if path_jadge is not None:
93
+ return path_jadge
94
+ 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.")
99
+ return ret
100
+
101
+ def check_apikey(self, req:Request, res:Response):
102
+ """
103
+ ApiKeyをチェックする
104
+
105
+ Args:
106
+ req (Request): リクエスト
107
+ res (Response): レスポンス
108
+
109
+ Returns:
110
+ Response: サインインエラーの場合はリダイレクトレスポンス
111
+ """
112
+ self.enable_cors(req, res)
113
+ if self.signin_file_data is None:
114
+ res.headers['signin'] = 'success'
115
+ return None
116
+ if 'Authorization' not in req.headers:
117
+ self.logger.warning(f"Authorization not found. headers={req.headers}")
118
+ return RedirectResponse(url=f'/signin{req.url.path}?error=noauth')
119
+ auth = req.headers['Authorization']
120
+ if not auth.startswith('Bearer '):
121
+ self.logger.warning(f"Bearer not found. headers={req.headers}")
122
+ return RedirectResponse(url=f'/signin{req.url.path}?error=apikeyfail')
123
+ bearer, apikey = auth.split(' ')
124
+ apikey = common.hash_password(apikey.strip(), 'sha1')
125
+ if self.logger.level == logging.DEBUG:
126
+ self.logger.debug(f"hashed apikey: {apikey}")
127
+ 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']:
130
+ if 'apikeys' not in user:
131
+ continue
132
+ for ak, key in user['apikeys'].items():
133
+ if apikey == key:
134
+ find_user = user
135
+ if find_user is None:
136
+ self.logger.warning(f"No matching user found for apikey.")
137
+ return RedirectResponse(url=f'/signin{req.url.path}?error=apikeyfail')
138
+
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]
141
+ req.session['signin'] = dict(uid=find_user['uid'], name=find_user['name'], password=find_user['password'],
142
+ 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}")
145
+ # パスルールチェック
146
+ user_groups = find_user['groups']
147
+ jadge = self.signin_file_data['pathrule']['policy']
148
+ for rule in self.signin_file_data['pathrule']['rules']:
149
+ if len([g for g in rule['groups'] if g in user_groups]) <= 0:
150
+ continue
151
+ if len([p for p in rule['paths'] if req.url.path.startswith(p)]) <= 0:
152
+ continue
153
+ jadge = rule['rule']
154
+ if self.logger.level == logging.DEBUG:
155
+ self.logger.debug(f"rule: {req.url.path}: {jadge}")
156
+ if jadge == 'allow':
157
+ res.headers['signin'] = 'success'
158
+ return None
159
+ self.logger.warning(f"Unauthorized site. user={find_user['name']}, path={req.url.path}")
160
+ return RedirectResponse(url=f'/signin{req.url.path}?error=unauthorizedsite')
161
+
162
+ @classmethod
163
+ def load_signin_file(cls, signin_file:Path, signin_file_data:Dict[str, Any]=None) -> Dict[str, Any]:
164
+ """
165
+ サインインファイルを読み込む
166
+
167
+ Args:
168
+ signin_file (Path): サインインファイル
169
+ signin_file_data (Dict[str, Any]): サインインファイルデータ
170
+
171
+ Raises:
172
+ HTTPException: サインインファイルのフォーマットエラー
173
+
174
+ Returns:
175
+ Dict[str, Any]: サインインファイルデータ
176
+ """
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
185
+ 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
421
+
422
+ @classmethod
423
+ def correct_group(cls, signin_file_data:Dict[str, Any], group_names:List[str], master_groups:List[Dict[str, Any]]) -> List[str]:
424
+ """
425
+ 指定されたグループ名に属する子グループ名を収集します
426
+
427
+ Args:
428
+ signin_file_data (Dict[str, Any]): サインインファイルデータ
429
+ group_names (List[str]): グループ名リスト
430
+ master_groups (List[Dict[str, Any]], optional): 親グループ名. Defaults to None.
431
+ """
432
+ copy_signin_data = copy.deepcopy(signin_file_data)
433
+ master_groups = copy_signin_data['groups'] if master_groups is None else master_groups
434
+ gns = []
435
+ for gn in group_names.copy():
436
+ gns = [gr['name'] for gr in master_groups if 'parent' in gr and gr['parent']==gn]
437
+ gns += cls.correct_group(copy_signin_data, gns, master_groups)
438
+ return group_names + gns
439
+
440
+ def check_path(self, req:Request, path:str) -> Union[None, RedirectResponse]:
441
+ """
442
+ パスの認可をチェックします
443
+
444
+ Args:
445
+ req (Request): リクエスト
446
+ path (str): パス
447
+
448
+ Returns:
449
+ Union[None, RedirectResponse]: 認可された場合はNone、認可されなかった場合はリダイレクトレスポンス
450
+ """
451
+ if self.signin_file_data is None:
452
+ return None
453
+ if 'signin' not in req.session:
454
+ return None
455
+ path = path if path.startswith('/') else f'/{path}'
456
+ # パスルールチェック
457
+ user_groups = req.session['signin']['groups']
458
+ jadge = self.signin_file_data['pathrule']['policy']
459
+ for rule in self.signin_file_data['pathrule']['rules']:
460
+ if len([g for g in rule['groups'] if g in user_groups]) <= 0:
461
+ continue
462
+ if len([p for p in rule['paths'] if path.startswith(p)]) <= 0:
463
+ continue
464
+ jadge = rule['rule']
465
+ if self.logger.level == logging.DEBUG:
466
+ self.logger.debug(f"rule: {path}: {jadge}")
467
+ if jadge == 'allow':
468
+ return None
469
+ else:
470
+ self.logger.warning(f"Unauthorized site. user={req.session['signin']['name']}, path={path}")
471
+ return RedirectResponse(url=f'/signin{path}?error=unauthorizedsite')
472
+
473
+ def check_cmd(self, req:Request, res:Response, mode:str, cmd:str):
474
+ """
475
+ コマンドの認可をチェックします
476
+
477
+ Args:
478
+ req (Request): リクエスト
479
+ res (Response): レスポンス
480
+ mode (str): モード
481
+ cmd (str): コマンド
482
+
483
+ Returns:
484
+ bool: 認可されたかどうか
485
+ """
486
+ if self.signin_file_data is None:
487
+ return True
488
+ if 'signin' not in req.session or 'groups' not in req.session['signin']:
489
+ return False
490
+ # コマンドチェック
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']:
494
+ if len([g for g in rule['groups'] if g in user_groups]) <= 0:
495
+ continue
496
+ if rule['mode'] is not None:
497
+ if rule['mode'] != mode:
498
+ continue
499
+ if len([c for c in rule['cmds'] if cmd == c]) <= 0:
500
+ continue
501
+ jadge = rule['rule']
502
+ if self.logger.level == logging.DEBUG:
503
+ self.logger.debug(f"rule: mode={mode}, cmd={cmd}: {jadge}")
504
+ return jadge == 'allow'
505
+
506
+ def get_enable_modes(self, req:Request, res:Response) -> List[str]:
507
+ """
508
+ 認可されたモードを取得します
509
+
510
+ Args:
511
+ req (Request): リクエスト
512
+ res (Response): レスポンス
513
+
514
+ Returns:
515
+ List[str]: 認可されたモード
516
+ """
517
+ if self.signin_file_data is None:
518
+ return self.options.get_modes().copy()
519
+ if 'signin' not in req.session or 'groups' not in req.session['signin']:
520
+ return []
521
+ modes = self.options.get_modes().copy()
522
+ user_groups = req.session['signin']['groups']
523
+ jadge = self.signin_file_data['cmdrule']['policy']
524
+ jadge_modes = []
525
+ if jadge == 'allow':
526
+ for m in modes:
527
+ jadge_modes += list(m.keys()) if type(m) is dict else [m]
528
+ for rule in self.signin_file_data['cmdrule']['rules']:
529
+ if len([g for g in rule['groups'] if g in user_groups]) <= 0:
530
+ continue
531
+ if 'mode' not in rule:
532
+ continue
533
+ if rule['mode'] is not None:
534
+ if rule['rule'] == 'allow':
535
+ jadge_modes.append(rule['mode'])
536
+ elif rule['rule'] == 'deny':
537
+ jadge_modes.remove(rule['mode'])
538
+ elif rule['mode'] is None and len(rule['cmds']) <= 0:
539
+ if rule['rule'] == 'allow':
540
+ for m in modes:
541
+ jadge_modes += list(m.keys()) if type(m) is dict else [m]
542
+ elif rule['rule'] == 'deny':
543
+ jadge_modes = []
544
+ return sorted(list(set(['']+jadge_modes)), key=lambda m: m)
545
+
546
+ def get_enable_cmds(self, mode:str, req:Request, res:Response) -> List[str]:
547
+ """
548
+ 認可されたコマンドを取得します
549
+
550
+ Args:
551
+ mode (str): モード
552
+ req (Request): リクエスト
553
+ res (Response): レスポンス
554
+
555
+ Returns:
556
+ List[str]: 認可されたコマンド
557
+ """
558
+ if self.signin_file_data is None:
559
+ cmds = self.options.get_cmds(mode).copy()
560
+ return cmds
561
+ if 'signin' not in req.session or 'groups' not in req.session['signin']:
562
+ return []
563
+ cmds = self.options.get_cmds(mode).copy()
564
+ if mode == '':
565
+ return cmds
566
+ user_groups = req.session['signin']['groups']
567
+ jadge = self.signin_file_data['cmdrule']['policy']
568
+ jadge_cmds = []
569
+ if jadge == 'allow':
570
+ for c in cmds:
571
+ jadge_cmds += list(c.keys()) if type(c) is dict else [c]
572
+ for rule in self.signin_file_data['cmdrule']['rules']:
573
+ if len([g for g in rule['groups'] if g in user_groups]) <= 0:
574
+ continue
575
+ if 'mode' not in rule:
576
+ continue
577
+ if 'cmds' not in rule:
578
+ continue
579
+ if rule['mode'] is not None and rule['mode'] != mode:
580
+ continue
581
+ if len(rule['cmds']) > 0:
582
+ if rule['rule'] == 'allow':
583
+ jadge_cmds += rule['cmds']
584
+ elif rule['rule'] == 'deny':
585
+ for c in rule['cmds']:
586
+ jadge_cmds.remove[c]
587
+ elif rule['mode'] is None and len(rule['cmds']) <= 0:
588
+ if rule['rule'] == 'allow':
589
+ for c in cmds:
590
+ jadge_cmds += list(c.keys()) if type(c) is dict else [c]
591
+ elif rule['rule'] == 'deny':
592
+ jadge_cmds = []
593
+ return sorted(list(set(['']+jadge_cmds)), key=lambda c: c)
594
+
595
+ def check_password_policy(self, user_name:str, password:str, new_password:str) -> Tuple[bool, str]:
596
+ """
597
+ パスワードポリシーをチェックする
598
+
599
+ Args:
600
+ user_name (str): ユーザー名
601
+ password (str): 元パスワード
602
+ new_password (str): 新しいパスワード
603
+ Returns:
604
+ bool: True:ポリシーOK, False:ポリシーNG
605
+ str: メッセージ
606
+ """
607
+ if self.signin_file_data is None or 'password' not in self.signin_file_data:
608
+ return True, "There is no password policy set."
609
+ policy = self.signin_file_data['password']['policy']
610
+ if not policy['enabled']:
611
+ return True, "Password policy is disabled."
612
+ if policy['not_same_before'] and password == new_password:
613
+ self.logger.warning(f"Password policy error. The same password cannot be changed.")
614
+ return False, f"Password policy error. The same password cannot be changed."
615
+ if len(new_password) < policy['min_length'] or len(new_password) > policy['max_length']:
616
+ self.logger.warning(f"Password policy error. min_length={policy['min_length']}, max_length={policy['max_length']}")
617
+ return False, f"Password policy error. min_length={policy['min_length']}, max_length={policy['max_length']}"
618
+ if len([c for c in new_password if c.islower()]) < policy['min_lowercase']:
619
+ self.logger.warning(f"Password policy error. min_lowercase={policy['min_lowercase']}")
620
+ return False, f"Password policy error. min_lowercase={policy['min_lowercase']}"
621
+ if len([c for c in new_password if c.isupper()]) < policy['min_uppercase']:
622
+ self.logger.warning(f"Password policy error. min_uppercase={policy['min_uppercase']}")
623
+ return False, f"Password policy error. min_uppercase={policy['min_uppercase']}"
624
+ if len([c for c in new_password if c.isdigit()]) < policy['min_digit']:
625
+ self.logger.warning(f"Password policy error. min_digit={policy['min_digit']}")
626
+ return False, f"Password policy error. min_digit={policy['min_digit']}"
627
+ if len([c for c in new_password if c in string.punctuation]) < policy['min_symbol']:
628
+ self.logger.warning(f"Password policy error. min_symbol={policy['min_symbol']}")
629
+ return False, f"Password policy error. min_symbol={policy['min_symbol']}"
630
+ if policy['not_contain_username'] and (user_name is None or user_name in new_password):
631
+ self.logger.warning(f"Password policy error. not_contain_username=True")
632
+ return False, f"Password policy error. not_contain_username=True"
633
+ self.logger.info(f"Password policy OK.")
634
+ return True, "Password policy OK."