cmdbox 0.5.1.1__py3-none-any.whl → 0.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cmdbox might be problematic. Click here for more details.
- cmdbox/app/app.py +4 -2
- cmdbox/app/auth/signin.py +633 -631
- cmdbox/app/client.py +10 -10
- cmdbox/app/common.py +50 -6
- cmdbox/app/commons/convert.py +9 -0
- cmdbox/app/commons/module.py +113 -113
- cmdbox/app/commons/redis_client.py +40 -29
- cmdbox/app/edge.py +4 -4
- cmdbox/app/features/cli/audit_base.py +135 -0
- cmdbox/app/features/cli/cmdbox_audit_createdb.py +224 -0
- cmdbox/app/features/cli/cmdbox_audit_delete.py +299 -0
- cmdbox/app/features/cli/cmdbox_audit_search.py +350 -0
- cmdbox/app/features/cli/cmdbox_audit_write.py +240 -0
- cmdbox/app/features/cli/cmdbox_client_file_copy.py +207 -207
- cmdbox/app/features/cli/cmdbox_client_file_download.py +207 -207
- cmdbox/app/features/cli/cmdbox_client_file_list.py +193 -193
- cmdbox/app/features/cli/cmdbox_client_file_mkdir.py +191 -191
- cmdbox/app/features/cli/cmdbox_client_file_move.py +199 -199
- cmdbox/app/features/cli/cmdbox_client_file_remove.py +190 -190
- cmdbox/app/features/cli/cmdbox_client_file_rmdir.py +190 -190
- cmdbox/app/features/cli/cmdbox_client_file_upload.py +212 -212
- cmdbox/app/features/cli/cmdbox_client_server_info.py +166 -166
- cmdbox/app/features/cli/cmdbox_server_list.py +88 -88
- cmdbox/app/features/cli/cmdbox_server_stop.py +138 -138
- cmdbox/app/features/web/cmdbox_web_del_cmd.py +2 -0
- cmdbox/app/features/web/cmdbox_web_del_pipe.py +1 -0
- cmdbox/app/features/web/cmdbox_web_do_signin.py +12 -2
- cmdbox/app/features/web/cmdbox_web_do_signout.py +1 -0
- cmdbox/app/features/web/cmdbox_web_exec_cmd.py +25 -1
- cmdbox/app/features/web/cmdbox_web_exec_pipe.py +1 -0
- cmdbox/app/features/web/cmdbox_web_filer download.py +43 -42
- cmdbox/app/features/web/cmdbox_web_filer.py +1 -0
- cmdbox/app/features/web/cmdbox_web_filer_upload.py +65 -64
- cmdbox/app/features/web/cmdbox_web_gui.py +166 -165
- cmdbox/app/features/web/cmdbox_web_load_pin.py +43 -43
- cmdbox/app/features/web/cmdbox_web_raw_pipe.py +87 -87
- cmdbox/app/features/web/cmdbox_web_save_cmd.py +1 -0
- cmdbox/app/features/web/cmdbox_web_save_pin.py +42 -42
- cmdbox/app/features/web/cmdbox_web_save_pipe.py +1 -0
- cmdbox/app/features/web/cmdbox_web_users.py +12 -0
- cmdbox/app/options.py +767 -601
- cmdbox/extensions/features.yml +20 -0
- cmdbox/extensions/sample_project/sample/app/features/cli/sample_client_time.py +82 -82
- cmdbox/extensions/sample_project/sample/app/features/cli/sample_server_time.py +145 -145
- cmdbox/licenses/{LICENSE.Sphinx.8.1.3(BSD License).txt → LICENSE.Sphinx.8.2.3(UNKNOWN).txt} +1 -1
- cmdbox/licenses/{LICENSE.babel.2.16.0(BSD License).txt → LICENSE.babel.2.17.0(BSD License).txt } +1 -1
- cmdbox/licenses/{LICENSE.pkginfo.1.10.0(MIT License).txt → LICENSE.charset-normalizer.3.4.1(MIT License).txt } +1 -1
- cmdbox/licenses/LICENSE.gunicorn.23.0.0(MIT License).txt +23 -0
- cmdbox/licenses/LICENSE.importlib_metadata.8.6.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.nh3.0.2.21(MIT).txt +21 -0
- cmdbox/licenses/{LICENSE.pillow.11.0.0(CMU License (MIT-CMU)).txt → LICENSE.pillow.11.1.0(CMU License (MIT-CMU)).txt } +27 -40
- cmdbox/licenses/LICENSE.plyer.2.1.0(MIT License).txt +19 -0
- cmdbox/licenses/LICENSE.prompt_toolkit.3.0.50(BSD License).txt +27 -0
- cmdbox/licenses/LICENSE.psycopg-binary.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +165 -0
- cmdbox/licenses/LICENSE.psycopg-pool.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +165 -0
- cmdbox/licenses/LICENSE.psycopg.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +165 -0
- cmdbox/licenses/LICENSE.pycryptodome.3.22.0(BSD License; Public Domain).txt +61 -0
- cmdbox/licenses/LICENSE.pystray.0.19.5(GNU Lesser General Public License v3 (LGPLv3)).txt +674 -0
- cmdbox/licenses/LICENSE.questionary.2.1.0(MIT License).txt +19 -0
- 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
- cmdbox/licenses/{LICENSE.six.1.16.0(MIT License).txt → LICENSE.six.1.17.0(MIT License).txt } +1 -1
- cmdbox/licenses/{LICENSE.charset-normalizer.3.4.0(MIT License).txt → LICENSE.typing-inspection.0.4.0(MIT License).txt } +2 -2
- cmdbox/licenses/LICENSE.tzdata.2025.2(Apache Software License).txt +15 -0
- cmdbox/licenses/files.txt +48 -36
- cmdbox/logconf_audit.yml +30 -0
- cmdbox/logconf_cmdbox.yml +30 -0
- cmdbox/version.py +2 -2
- cmdbox/web/assets/cmdbox/color_mode.css +516 -0
- cmdbox/web/assets/cmdbox/common.js +19 -0
- cmdbox/web/assets/cmdbox/list_cmd.js +89 -43
- cmdbox/web/assets/cmdbox/list_pipe.js +2 -1
- cmdbox/web/assets/cmdbox/main.js +2 -2
- cmdbox/web/assets/cmdbox/result.js +2 -2
- cmdbox/web/assets/cmdbox/signin.js +2 -2
- cmdbox/web/assets/cmdbox/users.js +2 -3
- cmdbox/web/assets/cmdbox/view_result.js +1 -1
- cmdbox/web/assets/filer/main.js +2 -2
- cmdbox/web/filer.html +16 -2
- cmdbox/web/gui.html +21 -2
- cmdbox/web/result.html +15 -1
- cmdbox/web/signin.html +35 -14
- cmdbox/web/users.html +15 -1
- {cmdbox-0.5.1.1.dist-info → cmdbox-0.5.2.dist-info}/METADATA +25 -5
- {cmdbox-0.5.1.1.dist-info → cmdbox-0.5.2.dist-info}/RECORD +117 -97
- {cmdbox-0.5.1.1.dist-info → cmdbox-0.5.2.dist-info}/entry_points.txt +0 -1
- cmdbox/licenses/LICENSE.nh3.0.2.18(MIT).txt +0 -1
- /cmdbox/licenses/{LICENSE.Jinja2.3.1.4(BSD License).txt → LICENSE.Jinja2.3.1.6(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.Pygments.2.18.0(BSD License).txt → LICENSE.Pygments.2.19.1(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.anyio.4.6.2.post1(MIT License).txt → LICENSE.anyio.4.9.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.argcomplete.3.5.1(Apache Software License).txt → LICENSE.argcomplete.3.6.1(Apache Software License).txt} +0 -0
- /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
- /cmdbox/licenses/{LICENSE.click.8.1.7(BSD License).txt → LICENSE.click.8.1.8(BSD License).txt} +0 -0
- /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
- /cmdbox/licenses/{LICENSE.fastapi.0.115.5(MIT License).txt → LICENSE.fastapi.0.115.12(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.importlib_metadata.8.5.0(Apache Software License).txt → LICENSE.id.1.5.0(Apache Software License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.keyring.25.5.0(MIT License).txt → LICENSE.keyring.25.6.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.more-itertools.10.5.0(MIT License).txt → LICENSE.more-itertools.10.6.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.numpy.2.1.3(BSD License).txt → LICENSE.numpy.2.2.4(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.prettytable.3.12.0(BSD License).txt → LICENSE.prettytable.3.16.0(UNKNOWN).txt} +0 -0
- /cmdbox/licenses/{LICENSE.pydantic.2.10.2(MIT License).txt → LICENSE.pydantic.2.11.1(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.pydantic_core.2.27.1(MIT License).txt → LICENSE.pydantic_core.2.33.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.python-dotenv.1.0.1(BSD License).txt → LICENSE.python-dotenv.1.1.0(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.python-multipart.0.0.17(Apache Software License).txt → LICENSE.python-multipart.0.0.20(Apache Software License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.redis.5.2.0(MIT License).txt → LICENSE.redis.5.2.1(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.rich.13.9.4(MIT License).txt → LICENSE.rich.14.0.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.sphinx-intl.2.3.0(BSD License).txt → LICENSE.sphinx-intl.2.3.1(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.starlette.0.41.3(BSD License).txt → LICENSE.starlette.0.46.1(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.tomli.2.1.0(MIT License).txt → LICENSE.tomli.2.2.1(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.twine.5.1.1(Apache Software License).txt → LICENSE.twine.6.1.0(Apache Software License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.typing_extensions.4.12.2(Python Software Foundation License).txt → LICENSE.typing_extensions.4.13.0(UNKNOWN).txt} +0 -0
- /cmdbox/licenses/{LICENSE.urllib3.2.2.3(MIT License).txt → LICENSE.urllib3.2.3.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.uvicorn.0.32.1(BSD License).txt → LICENSE.uvicorn.0.34.0(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.watchfiles.1.0.0(MIT License).txt → LICENSE.watchfiles.1.0.4(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.websockets.14.1(BSD License).txt → LICENSE.websockets.15.0.1(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.zope.interface.7.1.1(Zope Public License).txt → LICENSE.zope.interface.7.2(Zope Public License).txt} +0 -0
- {cmdbox-0.5.1.1.dist-info → cmdbox-0.5.2.dist-info}/LICENSE +0 -0
- {cmdbox-0.5.1.1.dist-info → cmdbox-0.5.2.dist-info}/WHEEL +0 -0
- {cmdbox-0.5.1.1.dist-info → cmdbox-0.5.2.dist-info}/top_level.txt +0 -0
cmdbox/app/auth/signin.py
CHANGED
|
@@ -1,631 +1,633 @@
|
|
|
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
|
|
295
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not
|
|
296
|
-
if
|
|
297
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
298
|
-
if
|
|
299
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not
|
|
300
|
-
if
|
|
301
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
302
|
-
if
|
|
303
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not
|
|
304
|
-
if
|
|
305
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
306
|
-
if
|
|
307
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not
|
|
308
|
-
if
|
|
309
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
310
|
-
if
|
|
311
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not
|
|
312
|
-
if
|
|
313
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
314
|
-
if
|
|
315
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not
|
|
316
|
-
if
|
|
317
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
318
|
-
if
|
|
319
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not
|
|
320
|
-
if
|
|
321
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
322
|
-
if
|
|
323
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not
|
|
324
|
-
if
|
|
325
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
326
|
-
if '
|
|
327
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
328
|
-
if
|
|
329
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not
|
|
330
|
-
if
|
|
331
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
332
|
-
if
|
|
333
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not
|
|
334
|
-
if
|
|
335
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
336
|
-
if
|
|
337
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not
|
|
338
|
-
if
|
|
339
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
340
|
-
if '
|
|
341
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
342
|
-
if
|
|
343
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not
|
|
344
|
-
if
|
|
345
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
346
|
-
if
|
|
347
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not
|
|
348
|
-
if
|
|
349
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
350
|
-
if
|
|
351
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if '
|
|
356
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if '
|
|
361
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
362
|
-
if
|
|
363
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not
|
|
364
|
-
if
|
|
365
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
366
|
-
if '
|
|
367
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
368
|
-
if '
|
|
369
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
370
|
-
if '
|
|
371
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
372
|
-
if
|
|
373
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not
|
|
374
|
-
if
|
|
375
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if '
|
|
380
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
381
|
-
if
|
|
382
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not
|
|
383
|
-
if
|
|
384
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
385
|
-
if '
|
|
386
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
387
|
-
if '
|
|
388
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
389
|
-
if '
|
|
390
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
391
|
-
if
|
|
392
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not
|
|
393
|
-
if
|
|
394
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if '
|
|
399
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
400
|
-
if
|
|
401
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not
|
|
402
|
-
if
|
|
403
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
404
|
-
if '
|
|
405
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
406
|
-
if '
|
|
407
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
408
|
-
if '
|
|
409
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
410
|
-
if '
|
|
411
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
412
|
-
if
|
|
413
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not
|
|
414
|
-
if
|
|
415
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if
|
|
451
|
-
return None
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if len([
|
|
460
|
-
continue
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if
|
|
486
|
-
return
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if rule['
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if
|
|
497
|
-
continue
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if
|
|
517
|
-
return
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if '
|
|
529
|
-
continue
|
|
530
|
-
if
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
jadge_modes.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if '
|
|
573
|
-
continue
|
|
574
|
-
if '
|
|
575
|
-
continue
|
|
576
|
-
if
|
|
577
|
-
continue
|
|
578
|
-
if
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if policy['
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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 'not_same_before' not in yml['password']['policy']:
|
|
295
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not found in "password.policy". ({signin_file})')
|
|
296
|
+
if type(yml['password']['policy']['not_same_before']) is not bool:
|
|
297
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not bool type in "password.policy". ({signin_file})')
|
|
298
|
+
if 'min_length' not in yml['password']['policy']:
|
|
299
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not found in "password.policy". ({signin_file})')
|
|
300
|
+
if type(yml['password']['policy']['min_length']) is not int:
|
|
301
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not int type in "password.policy". ({signin_file})')
|
|
302
|
+
if 'max_length' not in yml['password']['policy']:
|
|
303
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not found in "password.policy". ({signin_file})')
|
|
304
|
+
if type(yml['password']['policy']['max_length']) is not int:
|
|
305
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not int type in "password.policy". ({signin_file})')
|
|
306
|
+
if 'min_lowercase' not in yml['password']['policy']:
|
|
307
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not found in "password.policy". ({signin_file})')
|
|
308
|
+
if type(yml['password']['policy']['min_lowercase']) is not int:
|
|
309
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not int type in "password.policy". ({signin_file})')
|
|
310
|
+
if 'min_uppercase' not in yml['password']['policy']:
|
|
311
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not found in "password.policy". ({signin_file})')
|
|
312
|
+
if type(yml['password']['policy']['min_uppercase']) is not int:
|
|
313
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not int type in "password.policy". ({signin_file})')
|
|
314
|
+
if 'min_digit' not in yml['password']['policy']:
|
|
315
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not found in "password.policy". ({signin_file})')
|
|
316
|
+
if type(yml['password']['policy']['min_digit']) is not int:
|
|
317
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not int type in "password.policy". ({signin_file})')
|
|
318
|
+
if 'min_symbol' not in yml['password']['policy']:
|
|
319
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not found in "password.policy". ({signin_file})')
|
|
320
|
+
if type(yml['password']['policy']['min_symbol']) is not int:
|
|
321
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not int type in "password.policy". ({signin_file})')
|
|
322
|
+
if 'not_contain_username' not in yml['password']['policy']:
|
|
323
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not found in "password.policy". ({signin_file})')
|
|
324
|
+
if type(yml['password']['policy']['not_contain_username']) is not bool:
|
|
325
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not bool type in "password.policy". ({signin_file})')
|
|
326
|
+
if 'expiration' not in yml['password']:
|
|
327
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "expiration" not found in "password". ({signin_file})')
|
|
328
|
+
if 'enabled' not in yml['password']['expiration']:
|
|
329
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.expiration". ({signin_file})')
|
|
330
|
+
if type(yml['password']['expiration']['enabled']) is not bool:
|
|
331
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.expiration". ({signin_file})')
|
|
332
|
+
if 'period' not in yml['password']['expiration']:
|
|
333
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not found in "password.expiration". ({signin_file})')
|
|
334
|
+
if type(yml['password']['expiration']['period']) is not int:
|
|
335
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not int type in "password.expiration". ({signin_file})')
|
|
336
|
+
if 'notify' not in yml['password']['expiration']:
|
|
337
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not found in "password.expiration". ({signin_file})')
|
|
338
|
+
if type(yml['password']['expiration']['notify']) is not int:
|
|
339
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not int type in "password.expiration". ({signin_file})')
|
|
340
|
+
if 'lockout' not in yml['password']:
|
|
341
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "lockout" not found in "password". ({signin_file})')
|
|
342
|
+
if 'enabled' not in yml['password']['lockout']:
|
|
343
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.lockout". ({signin_file})')
|
|
344
|
+
if type(yml['password']['lockout']['enabled']) is not bool:
|
|
345
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.lockout". ({signin_file})')
|
|
346
|
+
if 'threshold' not in yml['password']['lockout']:
|
|
347
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not found in "password.lockout". ({signin_file})')
|
|
348
|
+
if type(yml['password']['lockout']['threshold']) is not int:
|
|
349
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not int type in "password.lockout". ({signin_file})')
|
|
350
|
+
if 'reset' not in yml['password']['lockout']:
|
|
351
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not found in "password.lockout". ({signin_file})')
|
|
352
|
+
if type(yml['password']['lockout']['reset']) is not int:
|
|
353
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not int type in "password.lockout". ({signin_file})')
|
|
354
|
+
# oauth2のフォーマットチェック
|
|
355
|
+
if 'oauth2' not in yml:
|
|
356
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "oauth2" not found. ({signin_file})')
|
|
357
|
+
if 'providers' not in yml['oauth2']:
|
|
358
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "providers" not found in "oauth2". ({signin_file})')
|
|
359
|
+
# google
|
|
360
|
+
if 'google' not in yml['oauth2']['providers']:
|
|
361
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "google" not found in "providers". ({signin_file})')
|
|
362
|
+
if 'enabled' not in yml['oauth2']['providers']['google']:
|
|
363
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "google". ({signin_file})')
|
|
364
|
+
if type(yml['oauth2']['providers']['google']['enabled']) is not bool:
|
|
365
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "google". ({signin_file})')
|
|
366
|
+
if 'client_id' not in yml['oauth2']['providers']['google']:
|
|
367
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "google". ({signin_file})')
|
|
368
|
+
if 'client_secret' not in yml['oauth2']['providers']['google']:
|
|
369
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "google". ({signin_file})')
|
|
370
|
+
if 'redirect_uri' not in yml['oauth2']['providers']['google']:
|
|
371
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "google". ({signin_file})')
|
|
372
|
+
if 'scope' not in yml['oauth2']['providers']['google']:
|
|
373
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "google". ({signin_file})')
|
|
374
|
+
if type(yml['oauth2']['providers']['google']['scope']) is not list:
|
|
375
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "google". ({signin_file})')
|
|
376
|
+
if 'signin_module' not in yml['oauth2']['providers']['google']:
|
|
377
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "google". ({signin_file})')
|
|
378
|
+
# github
|
|
379
|
+
if 'github' not in yml['oauth2']['providers']:
|
|
380
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "github" not found in "providers". ({signin_file})')
|
|
381
|
+
if 'enabled' not in yml['oauth2']['providers']['github']:
|
|
382
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "github". ({signin_file})')
|
|
383
|
+
if type(yml['oauth2']['providers']['github']['enabled']) is not bool:
|
|
384
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "github". ({signin_file})')
|
|
385
|
+
if 'client_id' not in yml['oauth2']['providers']['github']:
|
|
386
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "github". ({signin_file})')
|
|
387
|
+
if 'client_secret' not in yml['oauth2']['providers']['github']:
|
|
388
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "github". ({signin_file})')
|
|
389
|
+
if 'redirect_uri' not in yml['oauth2']['providers']['github']:
|
|
390
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "github". ({signin_file})')
|
|
391
|
+
if 'scope' not in yml['oauth2']['providers']['github']:
|
|
392
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "github". ({signin_file})')
|
|
393
|
+
if type(yml['oauth2']['providers']['github']['scope']) is not list:
|
|
394
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "github". ({signin_file})')
|
|
395
|
+
if 'signin_module' not in yml['oauth2']['providers']['github']:
|
|
396
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "github". ({signin_file})')
|
|
397
|
+
# azure
|
|
398
|
+
if 'azure' not in yml['oauth2']['providers']:
|
|
399
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "azure" not found in "providers". ({signin_file})')
|
|
400
|
+
if 'enabled' not in yml['oauth2']['providers']['azure']:
|
|
401
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "azure". ({signin_file})')
|
|
402
|
+
if type(yml['oauth2']['providers']['azure']['enabled']) is not bool:
|
|
403
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "azure". ({signin_file})')
|
|
404
|
+
if 'tenant_id' not in yml['oauth2']['providers']['azure']:
|
|
405
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "tenant_id" not found in "azure". ({signin_file})')
|
|
406
|
+
if 'client_id' not in yml['oauth2']['providers']['azure']:
|
|
407
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "azure". ({signin_file})')
|
|
408
|
+
if 'client_secret' not in yml['oauth2']['providers']['azure']:
|
|
409
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "azure". ({signin_file})')
|
|
410
|
+
if 'redirect_uri' not in yml['oauth2']['providers']['azure']:
|
|
411
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "azure". ({signin_file})')
|
|
412
|
+
if 'scope' not in yml['oauth2']['providers']['azure']:
|
|
413
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "azure". ({signin_file})')
|
|
414
|
+
if type(yml['oauth2']['providers']['azure']['scope']) is not list:
|
|
415
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "azure". ({signin_file})')
|
|
416
|
+
if 'signin_module' not in yml['oauth2']['providers']['azure']:
|
|
417
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "azure". ({signin_file})')
|
|
418
|
+
# フォーマットチェックOK
|
|
419
|
+
return yml
|
|
420
|
+
|
|
421
|
+
@classmethod
|
|
422
|
+
def correct_group(cls, signin_file_data:Dict[str, Any], group_names:List[str], master_groups:List[Dict[str, Any]]) -> List[str]:
|
|
423
|
+
"""
|
|
424
|
+
指定されたグループ名に属する子グループ名を収集します
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
signin_file_data (Dict[str, Any]): サインインファイルデータ
|
|
428
|
+
group_names (List[str]): グループ名リスト
|
|
429
|
+
master_groups (List[Dict[str, Any]], optional): 親グループ名. Defaults to None.
|
|
430
|
+
"""
|
|
431
|
+
copy_signin_data = copy.deepcopy(signin_file_data)
|
|
432
|
+
master_groups = copy_signin_data['groups'] if master_groups is None else master_groups
|
|
433
|
+
gns = []
|
|
434
|
+
for gn in group_names.copy():
|
|
435
|
+
gns = [gr['name'] for gr in master_groups if 'parent' in gr and gr['parent']==gn]
|
|
436
|
+
gns += cls.correct_group(copy_signin_data, gns, master_groups)
|
|
437
|
+
return group_names + gns
|
|
438
|
+
|
|
439
|
+
def check_path(self, req:Request, path:str) -> Union[None, RedirectResponse]:
|
|
440
|
+
"""
|
|
441
|
+
パスの認可をチェックします
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
req (Request): リクエスト
|
|
445
|
+
path (str): パス
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Union[None, RedirectResponse]: 認可された場合はNone、認可されなかった場合はリダイレクトレスポンス
|
|
449
|
+
"""
|
|
450
|
+
if self.signin_file_data is None:
|
|
451
|
+
return None
|
|
452
|
+
if 'signin' not in req.session:
|
|
453
|
+
return None
|
|
454
|
+
path = path if path.startswith('/') else f'/{path}'
|
|
455
|
+
# パスルールチェック
|
|
456
|
+
user_groups = req.session['signin']['groups']
|
|
457
|
+
jadge = self.signin_file_data['pathrule']['policy']
|
|
458
|
+
for rule in self.signin_file_data['pathrule']['rules']:
|
|
459
|
+
if len([g for g in rule['groups'] if g in user_groups]) <= 0:
|
|
460
|
+
continue
|
|
461
|
+
if len([p for p in rule['paths'] if path.startswith(p)]) <= 0:
|
|
462
|
+
continue
|
|
463
|
+
jadge = rule['rule']
|
|
464
|
+
if self.logger.level == logging.DEBUG:
|
|
465
|
+
self.logger.debug(f"rule: {path}: {jadge}")
|
|
466
|
+
if jadge == 'allow':
|
|
467
|
+
return None
|
|
468
|
+
else:
|
|
469
|
+
self.logger.warning(f"Unauthorized site. user={req.session['signin']['name']}, path={path}")
|
|
470
|
+
return RedirectResponse(url=f'/signin{path}?error=unauthorizedsite')
|
|
471
|
+
|
|
472
|
+
def check_cmd(self, req:Request, res:Response, mode:str, cmd:str):
|
|
473
|
+
"""
|
|
474
|
+
コマンドの認可をチェックします
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
req (Request): リクエスト
|
|
478
|
+
res (Response): レスポンス
|
|
479
|
+
mode (str): モード
|
|
480
|
+
cmd (str): コマンド
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
bool: 認可されたかどうか
|
|
484
|
+
"""
|
|
485
|
+
if self.signin_file_data is None:
|
|
486
|
+
return True
|
|
487
|
+
if 'signin' not in req.session or 'groups' not in req.session['signin']:
|
|
488
|
+
return False
|
|
489
|
+
# コマンドチェック
|
|
490
|
+
user_groups = req.session['signin']['groups']
|
|
491
|
+
jadge = self.signin_file_data['cmdrule']['policy']
|
|
492
|
+
for rule in self.signin_file_data['cmdrule']['rules']:
|
|
493
|
+
if len([g for g in rule['groups'] if g in user_groups]) <= 0:
|
|
494
|
+
continue
|
|
495
|
+
if rule['mode'] is not None:
|
|
496
|
+
if rule['mode'] != mode:
|
|
497
|
+
continue
|
|
498
|
+
if len([c for c in rule['cmds'] if cmd == c]) <= 0:
|
|
499
|
+
continue
|
|
500
|
+
jadge = rule['rule']
|
|
501
|
+
if self.logger.level == logging.DEBUG:
|
|
502
|
+
self.logger.debug(f"rule: mode={mode}, cmd={cmd}: {jadge}")
|
|
503
|
+
return jadge == 'allow'
|
|
504
|
+
|
|
505
|
+
def get_enable_modes(self, req:Request, res:Response) -> List[str]:
|
|
506
|
+
"""
|
|
507
|
+
認可されたモードを取得します
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
req (Request): リクエスト
|
|
511
|
+
res (Response): レスポンス
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
List[str]: 認可されたモード
|
|
515
|
+
"""
|
|
516
|
+
if self.signin_file_data is None:
|
|
517
|
+
return self.options.get_modes().copy()
|
|
518
|
+
if 'signin' not in req.session or 'groups' not in req.session['signin']:
|
|
519
|
+
return []
|
|
520
|
+
modes = self.options.get_modes().copy()
|
|
521
|
+
user_groups = req.session['signin']['groups']
|
|
522
|
+
jadge = self.signin_file_data['cmdrule']['policy']
|
|
523
|
+
jadge_modes = []
|
|
524
|
+
if jadge == 'allow':
|
|
525
|
+
for m in modes:
|
|
526
|
+
jadge_modes += list(m.keys()) if type(m) is dict else [m]
|
|
527
|
+
for rule in self.signin_file_data['cmdrule']['rules']:
|
|
528
|
+
if len([g for g in rule['groups'] if g in user_groups]) <= 0:
|
|
529
|
+
continue
|
|
530
|
+
if 'mode' not in rule:
|
|
531
|
+
continue
|
|
532
|
+
if rule['mode'] is not None:
|
|
533
|
+
if rule['rule'] == 'allow':
|
|
534
|
+
jadge_modes.append(rule['mode'])
|
|
535
|
+
elif rule['rule'] == 'deny':
|
|
536
|
+
jadge_modes.remove(rule['mode'])
|
|
537
|
+
elif rule['mode'] is None and len(rule['cmds']) <= 0:
|
|
538
|
+
if rule['rule'] == 'allow':
|
|
539
|
+
for m in modes:
|
|
540
|
+
jadge_modes += list(m.keys()) if type(m) is dict else [m]
|
|
541
|
+
elif rule['rule'] == 'deny':
|
|
542
|
+
jadge_modes = []
|
|
543
|
+
return sorted(list(set(['']+jadge_modes)), key=lambda m: m)
|
|
544
|
+
|
|
545
|
+
def get_enable_cmds(self, mode:str, req:Request, res:Response) -> List[str]:
|
|
546
|
+
"""
|
|
547
|
+
認可されたコマンドを取得します
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
mode (str): モード
|
|
551
|
+
req (Request): リクエスト
|
|
552
|
+
res (Response): レスポンス
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
List[str]: 認可されたコマンド
|
|
556
|
+
"""
|
|
557
|
+
if self.signin_file_data is None:
|
|
558
|
+
cmds = self.options.get_cmds(mode).copy()
|
|
559
|
+
return cmds
|
|
560
|
+
if 'signin' not in req.session or 'groups' not in req.session['signin']:
|
|
561
|
+
return []
|
|
562
|
+
cmds = self.options.get_cmds(mode).copy()
|
|
563
|
+
if mode == '':
|
|
564
|
+
return cmds
|
|
565
|
+
user_groups = req.session['signin']['groups']
|
|
566
|
+
jadge = self.signin_file_data['cmdrule']['policy']
|
|
567
|
+
jadge_cmds = []
|
|
568
|
+
if jadge == 'allow':
|
|
569
|
+
for c in cmds:
|
|
570
|
+
jadge_cmds += list(c.keys()) if type(c) is dict else [c]
|
|
571
|
+
for rule in self.signin_file_data['cmdrule']['rules']:
|
|
572
|
+
if len([g for g in rule['groups'] if g in user_groups]) <= 0:
|
|
573
|
+
continue
|
|
574
|
+
if 'mode' not in rule:
|
|
575
|
+
continue
|
|
576
|
+
if 'cmds' not in rule:
|
|
577
|
+
continue
|
|
578
|
+
if rule['mode'] is not None and rule['mode'] != mode:
|
|
579
|
+
continue
|
|
580
|
+
if len(rule['cmds']) > 0:
|
|
581
|
+
if rule['rule'] == 'allow':
|
|
582
|
+
jadge_cmds += rule['cmds']
|
|
583
|
+
elif rule['rule'] == 'deny':
|
|
584
|
+
for c in rule['cmds']:
|
|
585
|
+
jadge_cmds.remove[c]
|
|
586
|
+
elif rule['mode'] is None and len(rule['cmds']) <= 0:
|
|
587
|
+
if rule['rule'] == 'allow':
|
|
588
|
+
for c in cmds:
|
|
589
|
+
jadge_cmds += list(c.keys()) if type(c) is dict else [c]
|
|
590
|
+
elif rule['rule'] == 'deny':
|
|
591
|
+
jadge_cmds = []
|
|
592
|
+
return sorted(list(set(['']+jadge_cmds)), key=lambda c: c)
|
|
593
|
+
|
|
594
|
+
def check_password_policy(self, user_name:str, password:str, new_password:str) -> Tuple[bool, str]:
|
|
595
|
+
"""
|
|
596
|
+
パスワードポリシーをチェックする
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
user_name (str): ユーザー名
|
|
600
|
+
password (str): 元パスワード
|
|
601
|
+
new_password (str): 新しいパスワード
|
|
602
|
+
Returns:
|
|
603
|
+
bool: True:ポリシーOK, False:ポリシーNG
|
|
604
|
+
str: メッセージ
|
|
605
|
+
"""
|
|
606
|
+
if self.signin_file_data is None or 'password' not in self.signin_file_data:
|
|
607
|
+
return True, "There is no password policy set."
|
|
608
|
+
policy = self.signin_file_data['password']['policy']
|
|
609
|
+
if not policy['enabled']:
|
|
610
|
+
return True, "Password policy is disabled."
|
|
611
|
+
if policy['not_same_before'] and password == new_password:
|
|
612
|
+
self.logger.warning(f"Password policy error. The same password cannot be changed.")
|
|
613
|
+
return False, f"Password policy error. The same password cannot be changed."
|
|
614
|
+
if len(new_password) < policy['min_length'] or len(new_password) > policy['max_length']:
|
|
615
|
+
self.logger.warning(f"Password policy error. min_length={policy['min_length']}, max_length={policy['max_length']}")
|
|
616
|
+
return False, f"Password policy error. min_length={policy['min_length']}, max_length={policy['max_length']}"
|
|
617
|
+
if len([c for c in new_password if c.islower()]) < policy['min_lowercase']:
|
|
618
|
+
self.logger.warning(f"Password policy error. min_lowercase={policy['min_lowercase']}")
|
|
619
|
+
return False, f"Password policy error. min_lowercase={policy['min_lowercase']}"
|
|
620
|
+
if len([c for c in new_password if c.isupper()]) < policy['min_uppercase']:
|
|
621
|
+
self.logger.warning(f"Password policy error. min_uppercase={policy['min_uppercase']}")
|
|
622
|
+
return False, f"Password policy error. min_uppercase={policy['min_uppercase']}"
|
|
623
|
+
if len([c for c in new_password if c.isdigit()]) < policy['min_digit']:
|
|
624
|
+
self.logger.warning(f"Password policy error. min_digit={policy['min_digit']}")
|
|
625
|
+
return False, f"Password policy error. min_digit={policy['min_digit']}"
|
|
626
|
+
if len([c for c in new_password if c in string.punctuation]) < policy['min_symbol']:
|
|
627
|
+
self.logger.warning(f"Password policy error. min_symbol={policy['min_symbol']}")
|
|
628
|
+
return False, f"Password policy error. min_symbol={policy['min_symbol']}"
|
|
629
|
+
if policy['not_contain_username'] and (user_name is None or user_name in new_password):
|
|
630
|
+
self.logger.warning(f"Password policy error. not_contain_username=True")
|
|
631
|
+
return False, f"Password policy error. not_contain_username=True"
|
|
632
|
+
self.logger.info(f"Password policy OK.")
|
|
633
|
+
return True, "Password policy OK."
|