cmdbox 0.5.1__py3-none-any.whl → 0.5.1.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/auth/signin.py +631 -0
- cmdbox/version.py +2 -2
- cmdbox/web/assets/cmdbox/list_cmd.js +80 -33
- cmdbox/web/assets/cmdbox/list_pipe.js +2 -1
- cmdbox/web/gui.html +6 -1
- {cmdbox-0.5.1.dist-info → cmdbox-0.5.1.2.dist-info}/METADATA +1 -1
- {cmdbox-0.5.1.dist-info → cmdbox-0.5.1.2.dist-info}/RECORD +11 -10
- {cmdbox-0.5.1.dist-info → cmdbox-0.5.1.2.dist-info}/LICENSE +0 -0
- {cmdbox-0.5.1.dist-info → cmdbox-0.5.1.2.dist-info}/WHEEL +0 -0
- {cmdbox-0.5.1.dist-info → cmdbox-0.5.1.2.dist-info}/entry_points.txt +0 -0
- {cmdbox-0.5.1.dist-info → cmdbox-0.5.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,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 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."
|
cmdbox/version.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
|
|
3
|
-
dt_now = datetime.datetime(2025, 3,
|
|
3
|
+
dt_now = datetime.datetime(2025, 3, 27)
|
|
4
4
|
__appid__ = 'cmdbox'
|
|
5
5
|
__title__ = 'cmdbox (Command Development Application)'
|
|
6
|
-
__version__ = '0.5.1'
|
|
6
|
+
__version__ = '0.5.1.2'
|
|
7
7
|
__copyright__ = f'Copyright © 2023-{dt_now.strftime("%Y")} hamacom2004jp'
|
|
8
8
|
__pypiurl__ = 'https://pypi.org/project/cmdbox/'
|
|
9
9
|
__srcurl__ = 'https://github.com/hamacom2004jp/cmdbox'
|
|
@@ -15,7 +15,14 @@ const list_cmd_func = async () => {
|
|
|
15
15
|
}
|
|
16
16
|
elem.find('.cmd_title').text(row.title);
|
|
17
17
|
elem.find('.cmd_mode').text(row.mode);
|
|
18
|
-
elem.find('.cmd_cmd').text(row.cmd)
|
|
18
|
+
elem.find('.cmd_cmd').text(row.cmd);
|
|
19
|
+
const tags_elem = elem.find('.cmd_tags');
|
|
20
|
+
if (row.tag && Array.isArray(row.tag)) {
|
|
21
|
+
row.tag.forEach(tag => {
|
|
22
|
+
if (tag=='') return;
|
|
23
|
+
tags_elem.append(`<span class="badge text-bg-secondary me-1"><svg class="bi bi-svg_tag" width="16" height="16" fill="currentColor"><use href="#svg_tag"></use></svg>${tag}</span>`);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
19
26
|
if (row.tag && Array.isArray(row.tag)) {
|
|
20
27
|
const tags = new Set([...row.tag]);
|
|
21
28
|
elem.find('.cmd_card').attr('data-tags', Array.from(tags).join(','));
|
|
@@ -24,49 +31,89 @@ const list_cmd_func = async () => {
|
|
|
24
31
|
};
|
|
25
32
|
py_list_cmd.forEach(row => {card_func(row, true)});
|
|
26
33
|
py_list_cmd.forEach(row => {card_func(row, false)});
|
|
27
|
-
$('#cmd_item_tags').html('');
|
|
34
|
+
const cmd_item_tags = $('#cmd_item_tags').html('');
|
|
35
|
+
const tag_bot_click = (e) => {
|
|
36
|
+
const ct = $(e.currentTarget);
|
|
37
|
+
const cmd_items = $('#cmd_items').find('.cmd_card:not(.cmd_add)');
|
|
38
|
+
const andor_sw = ct.hasClass('andor_switch');
|
|
39
|
+
const and_val = $('#andor_switch').prop('checked');
|
|
40
|
+
// 選択中のボタンの場合は解除中にする
|
|
41
|
+
if (!andor_sw && ct.hasClass('btn-secondary')) {
|
|
42
|
+
ct.removeClass('btn-secondary');
|
|
43
|
+
ct.addClass('btn-outline-secondary');
|
|
44
|
+
}
|
|
45
|
+
// 解除中のボタンの場合は選択中にする
|
|
46
|
+
else if (!andor_sw && ct.hasClass('btn-outline-secondary')) {
|
|
47
|
+
ct.removeClass('btn-outline-secondary');
|
|
48
|
+
ct.addClass('btn-secondary');
|
|
49
|
+
}
|
|
50
|
+
// 選択中のタグを取得
|
|
51
|
+
const tags = new Set();
|
|
52
|
+
cmd_item_tags.find('.btn-tag').each((i, elem) => {
|
|
53
|
+
if ($(elem).hasClass('btn-secondary')) tags.add($(elem).attr('data-tag'));
|
|
54
|
+
});
|
|
55
|
+
// タグ無しボタンが選択された場合
|
|
56
|
+
if (ct.hasClass('btn_notag') && ct.hasClass('btn-secondary')) {
|
|
57
|
+
cmd_item_tags.find('.btn-tag').removeClass('btn-secondary').addClass('btn-outline-secondary');
|
|
58
|
+
cmd_items.parent().hide();
|
|
59
|
+
cmd_items.each((i, elem) => {
|
|
60
|
+
const el = $(elem);
|
|
61
|
+
const itags = el.attr('data-tags');
|
|
62
|
+
if (itags) return;
|
|
63
|
+
el.parent().show();
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// notagボタンの選択を解除
|
|
68
|
+
if (!andor_sw) $('#btn_notag').removeClass('btn-secondary').addClass('btn-outline-secondary');
|
|
69
|
+
// タグがない場合は全て表示
|
|
70
|
+
if (tags.size == 0) {
|
|
71
|
+
cmd_items.parent().show();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// タグがある場合はタグに一致するものだけ表示
|
|
75
|
+
cmd_items.parent().hide();
|
|
76
|
+
cmd_items.each((i, elem) => {
|
|
77
|
+
const el = $(elem);
|
|
78
|
+
// andorチェックされている場合はすべてのタグを含むものを表示
|
|
79
|
+
if (and_val) {
|
|
80
|
+
const itags = el.attr('data-tags');
|
|
81
|
+
if (!itags) return;
|
|
82
|
+
const etags = itags.split(',');
|
|
83
|
+
if (etags.filter(tag => tags.has(tag)).length == tags.size) el.parent().show();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// andorチェックされていない場合はいずれかのタグを含むものを表示
|
|
87
|
+
tags.forEach(tag => {
|
|
88
|
+
const itags = el.attr('data-tags');
|
|
89
|
+
if (!itags) return;
|
|
90
|
+
else if (itags.split(',').includes(tag)) el.parent().show();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
// タグボタンを追加
|
|
28
95
|
py_list_cmd.forEach(row => {
|
|
29
96
|
if (!row.tag || !Array.isArray(row.tag)) return;
|
|
30
|
-
const cmd_item_tags = $('#cmd_item_tags');
|
|
31
97
|
row.tag.forEach(tag => {
|
|
32
98
|
if (tag=='') return;
|
|
33
99
|
if (cmd_item_tags.find(`[data-tag="${tag}"]`).length > 0) return;
|
|
34
100
|
const elem = $(`<button type="button" class="btn btn-outline-secondary btn-sm btn-tag me-2">${tag}</button>`);
|
|
35
101
|
elem.attr('data-tag', tag);
|
|
36
102
|
elem.text(tag);
|
|
37
|
-
elem.click(
|
|
38
|
-
const ct = $(e.currentTarget);
|
|
39
|
-
const cmd_items = $('#cmd_items').find('.cmd_card:not(.cmd_add)');
|
|
40
|
-
if (ct.hasClass('btn-secondary')) {
|
|
41
|
-
ct.removeClass('btn-secondary');
|
|
42
|
-
ct.addClass('btn-outline-secondary');
|
|
43
|
-
}
|
|
44
|
-
else if (ct.hasClass('btn-outline-secondary')) {
|
|
45
|
-
ct.removeClass('btn-outline-secondary');
|
|
46
|
-
ct.addClass('btn-secondary');
|
|
47
|
-
}
|
|
48
|
-
const tags = new Set();
|
|
49
|
-
cmd_item_tags.find('.btn-tag').each((i, elem) => {
|
|
50
|
-
if ($(elem).hasClass('btn-secondary')) tags.add($(elem).attr('data-tag'));
|
|
51
|
-
});
|
|
52
|
-
if (tags.size == 0) {
|
|
53
|
-
cmd_items.parent().show();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
cmd_items.parent().hide();
|
|
57
|
-
tags.forEach(tag => {
|
|
58
|
-
cmd_items.each((i, elem) => {
|
|
59
|
-
const el = $(elem);
|
|
60
|
-
const itags = el.attr('data-tags');
|
|
61
|
-
if (!itags) return;
|
|
62
|
-
else if (itags.split(',').includes(tag)) el.parent().show();
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
});
|
|
103
|
+
elem.click(tag_bot_click);
|
|
66
104
|
cmd_item_tags.append(elem);
|
|
67
105
|
});
|
|
68
|
-
|
|
69
106
|
});
|
|
107
|
+
// タグ未選択ボタンを追加
|
|
108
|
+
const noselect_bot = $(`<button type="button" class="btn btn-outline-secondary btn-sm btn_notag me-2" id="btn_notag">no tag</button>`);
|
|
109
|
+
noselect_bot.click(tag_bot_click);
|
|
110
|
+
cmd_item_tags.append(noselect_bot);
|
|
111
|
+
// and/orスイッチを追加
|
|
112
|
+
const andor_bot = $(`<div class="form-check form-switch text-secondary d-inline-block"/>`);
|
|
113
|
+
andor_bot.append('<input class="form-check-input andor_switch" type="checkbox" id="andor_switch">');
|
|
114
|
+
andor_bot.append('<label class="form-check-label" for="andor_switch">or / and</label>');
|
|
115
|
+
andor_bot.find('#andor_switch').click(tag_bot_click);
|
|
116
|
+
cmd_item_tags.append(andor_bot);
|
|
70
117
|
}
|
|
71
118
|
// コマンドファイルの取得が出来た時の処理
|
|
72
119
|
const list_cmd_func_then = () => {
|
|
@@ -45,7 +45,8 @@ list_pipe_func_then = () => {
|
|
|
45
45
|
const option = $('<option></option>');
|
|
46
46
|
pipe_cmd_select.append(option);
|
|
47
47
|
option.attr('value', cmd['title']);
|
|
48
|
-
|
|
48
|
+
const tag = cmd['tag'] ? `, tag=${cmd['tag']}` : '';
|
|
49
|
+
option.text(`${cmd['title']}(mode=${cmd['mode']}, cmd=${cmd['cmd']}${tag})`);
|
|
49
50
|
});
|
|
50
51
|
cmd_select_template.find('.add_buton').click((e) => {
|
|
51
52
|
cmd_select_template_func($(e.currentTarget), py_list_cmd);
|
cmdbox/web/gui.html
CHANGED
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
<symbol id="svg_magic_btn" viewBox="0 0 16 16">
|
|
52
52
|
<path d="M9.5 2.672a.5.5 0 1 0 1 0V.843a.5.5 0 0 0-1 0zm4.5.035A.5.5 0 0 0 13.293 2L12 3.293a.5.5 0 1 0 .707.707zM7.293 4A.5.5 0 1 0 8 3.293L6.707 2A.5.5 0 0 0 6 2.707zm-.621 2.5a.5.5 0 1 0 0-1H4.843a.5.5 0 1 0 0 1zm8.485 0a.5.5 0 1 0 0-1h-1.829a.5.5 0 0 0 0 1zM13.293 10A.5.5 0 1 0 14 9.293L12.707 8a.5.5 0 1 0-.707.707zM9.5 11.157a.5.5 0 0 0 1 0V9.328a.5.5 0 0 0-1 0zm1.854-5.097a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L8.646 5.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0l1.293-1.293Zm-3 3a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L.646 13.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0z"/>
|
|
53
53
|
</symbol>
|
|
54
|
+
<symbol id="svg_tag" viewBox="0 0 16 16">
|
|
55
|
+
<path d="M6 4.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m-1 0a.5.5 0 1 0-1 0 .5.5 0 0 0 1 0"/>
|
|
56
|
+
<path d="M2 1h4.586a1 1 0 0 1 .707.293l7 7a1 1 0 0 1 0 1.414l-4.586 4.586a1 1 0 0 1-1.414 0l-7-7A1 1 0 0 1 1 6.586V2a1 1 0 0 1 1-1m0 5.586 7 7L13.586 9l-7-7H2z"/>
|
|
57
|
+
</symbol>
|
|
54
58
|
</svg>
|
|
55
59
|
<!-- ナビゲーションバー -->
|
|
56
60
|
<nav class="navbar navbar-expand-sm fixed-top p-2">
|
|
@@ -133,7 +137,8 @@
|
|
|
133
137
|
<div class="d-flex">
|
|
134
138
|
<h5 class="cmd_title card-title d-inline-block">Card title</h5>
|
|
135
139
|
</div>
|
|
136
|
-
<h6 class="card-subtitle mb-2
|
|
140
|
+
<h6 class="card-subtitle mb-2">mode:<span class="cmd_mode"></span>, cmd:<span class="cmd_cmd"></span></h6>
|
|
141
|
+
<h6 class="card-subtitle text-end"><span class="cmd_tags text-secondary"></span></h6>
|
|
137
142
|
</div>
|
|
138
143
|
</div>
|
|
139
144
|
</div>
|
|
@@ -6,7 +6,7 @@ cmdbox/logconf_edge.yml,sha256=RkbUebCJV2z5dLnpgvf4GSFkd-Blzu4vUUDP9CQFwdM,686
|
|
|
6
6
|
cmdbox/logconf_gui.yml,sha256=T3yhWoiyp0DW06RjiFG6kS7jScqXYs-KLfC5EYKUImk,686
|
|
7
7
|
cmdbox/logconf_server.yml,sha256=tpDpKQXgTWzUnHKGU-Vvsha7n1hyIyFdLnSeCgnOgyk,701
|
|
8
8
|
cmdbox/logconf_web.yml,sha256=lzr3ytjqRbQutbhEOJdHJT0hrrR_h9sPkaEQkzX02lo,686
|
|
9
|
-
cmdbox/version.py,sha256=
|
|
9
|
+
cmdbox/version.py,sha256=avcUh9fYN64SEpQ8Zte4PMgniHFaWsKlaZe8xB7iC9w,1963
|
|
10
10
|
cmdbox/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
cmdbox/app/app.py,sha256=2rQpTbezCLuy36df89EhKsAfcPjtGepGHSTQZXimcqA,8949
|
|
12
12
|
cmdbox/app/client.py,sha256=SNM4xxDh5ARxV6I7cDrmbzGc7sXUUwB3tSyIksdT1RY,19562
|
|
@@ -17,6 +17,7 @@ cmdbox/app/filer.py,sha256=L_DSMTvnbN_ffr3JIt0obbOmVoTHEfVm2cAVz3rLH-Q,16059
|
|
|
17
17
|
cmdbox/app/options.py,sha256=3vwQx5ekrwurSHJLEsd3iXC2f8QevkAiR08ZioTKYPo,30874
|
|
18
18
|
cmdbox/app/server.py,sha256=rrH_a6zzrx7glp_SqRrc_2gBgRoT9FX0F1bpGxVLX1I,9467
|
|
19
19
|
cmdbox/app/web.py,sha256=9Zu9z7Pkdmz1T6NbJ6cn4rSLOAflrKG8veb5b4pQOqk,45469
|
|
20
|
+
cmdbox/app/auth/signin.py,sha256=5G3Wxpi0H3rlvbcVyuBzTvRnttMpJJQuq8njSorEfOI,40713
|
|
20
21
|
cmdbox/app/commons/convert.py,sha256=etWeutkPyE8FMz11jw4KJ5uip7qu_CoD1yIqF5iypgg,6754
|
|
21
22
|
cmdbox/app/commons/loghandler.py,sha256=HFTlsQEshFaIubCH-AjvXOE0swbYtv77kSy2pwRz_8c,3469
|
|
22
23
|
cmdbox/app/commons/module.py,sha256=RsEyqP9Qty4v0qIEE6VuY4pgMlkhJKNTUhsN5p-sSoM,4880
|
|
@@ -185,7 +186,7 @@ cmdbox/licenses/LICENSE.zope.interface.7.1.1(Zope Public License).txt,sha256=bHk
|
|
|
185
186
|
cmdbox/licenses/files.txt,sha256=E_C-9JE8iJeOs-ZY_Lg1B3Brcdqr8G-kJeFnWeSHXUk,14179
|
|
186
187
|
cmdbox/web/assets_license_list.txt,sha256=TcyF-U5C8O-J3Nxfp3pxralGdazTj8BgLwwEBRn59_w,853
|
|
187
188
|
cmdbox/web/filer.html,sha256=NQzwJc1k4IDpK1F99vumLC3BO7x_frO6HJcEzI9c0kw,15254
|
|
188
|
-
cmdbox/web/gui.html,sha256=
|
|
189
|
+
cmdbox/web/gui.html,sha256=Wwb70-gH66yaYTfLwFdDXXPdEyVEhllOtuKxq4VAUoE,35141
|
|
189
190
|
cmdbox/web/result.html,sha256=dZcQCLSrhTr7gY50WYCfG2wEWngnV7x9xnYOVl73WSQ,14835
|
|
190
191
|
cmdbox/web/signin.html,sha256=Rhqvhcq_hZ1svRkHIpy_2BVTJkmw2R4Q4lZo93eQvsE,11063
|
|
191
192
|
cmdbox/web/users.html,sha256=RHslVrj8FJgV1RI6oMTRuZTu3g7YyXMKKYGeoOzgwG4,19194
|
|
@@ -195,8 +196,8 @@ cmdbox/web/assets/cmdbox/common.js,sha256=rsegebcs7QtBNi-WEy5-sev-EDIEHV9P-0HZoj
|
|
|
195
196
|
cmdbox/web/assets/cmdbox/favicon.ico,sha256=2U4MhqzJklRksOQwnK-MZigZCubxCHqKG_AuNJnvYtA,34494
|
|
196
197
|
cmdbox/web/assets/cmdbox/filer_modal.js,sha256=hCEk0UR_bjJp9X9blWfmrbZVxKkmO3GGQ44ukSDuHbg,8522
|
|
197
198
|
cmdbox/web/assets/cmdbox/icon.png,sha256=xdEwDdCS8CoPQk7brW-1mV8FIGYtUeSMBRlY9Oh-3nE,296172
|
|
198
|
-
cmdbox/web/assets/cmdbox/list_cmd.js,sha256=
|
|
199
|
-
cmdbox/web/assets/cmdbox/list_pipe.js,sha256=
|
|
199
|
+
cmdbox/web/assets/cmdbox/list_cmd.js,sha256=SkUVr9gkakzzCZAMWGcCYPepbElHy4o1VbP-idTRSco,35334
|
|
200
|
+
cmdbox/web/assets/cmdbox/list_pipe.js,sha256=1Jq9MbJDtD8tsI2HB_qFUDhJ9MY3V14-zQjPZHgbrIo,11294
|
|
200
201
|
cmdbox/web/assets/cmdbox/main.js,sha256=K-tBnZlCpy__juT_HHKfvHzhNJykAjva7LUBJX45XzM,4997
|
|
201
202
|
cmdbox/web/assets/cmdbox/open_capture.js,sha256=W4IQlOYLN4Y8OaS8Xc5yp-BRlm82TVjujChr3hJKS0M,709
|
|
202
203
|
cmdbox/web/assets/cmdbox/open_output_json.js,sha256=4q7mCdVmSzFudlTlW9MuIJ1-f-kDvpD6rDUU01IbKi8,727
|
|
@@ -247,9 +248,9 @@ cmdbox/web/assets/tree-menu/image/file.png,sha256=Uw4zYkHyuoZ_kSVkesHAeSeA_g9_LP
|
|
|
247
248
|
cmdbox/web/assets/tree-menu/image/folder-close.png,sha256=TcgsKTBBF2ejgzekOEDBFBxsJf-Z5u0x9IZVi4GBR-I,284
|
|
248
249
|
cmdbox/web/assets/tree-menu/image/folder-open.png,sha256=DT7y1GRK4oXJkFvqTN_oSGM5ZYARzPvjoCGL6wqkoo0,301
|
|
249
250
|
cmdbox/web/assets/tree-menu/js/tree-menu.js,sha256=-GkZxI7xzHuXXHYQBHAVTcuKX4TtoiMuyIms6Xc3pxk,1029
|
|
250
|
-
cmdbox-0.5.1.dist-info/LICENSE,sha256=sBzzPc5v-5LBuIFi2V4olsnoVg-3EBI0zRX5r19SOxE,1117
|
|
251
|
-
cmdbox-0.5.1.dist-info/METADATA,sha256=
|
|
252
|
-
cmdbox-0.5.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
253
|
-
cmdbox-0.5.1.dist-info/entry_points.txt,sha256=PIoRz-tr503YwdMmd6nxuSn2dDltf4cUMVs98E9WgaA,48
|
|
254
|
-
cmdbox-0.5.1.dist-info/top_level.txt,sha256=eMEkD5jn8_0PkCAL8h5xJu4qAzF2O8Wf3vegFkKUXR4,7
|
|
255
|
-
cmdbox-0.5.1.dist-info/RECORD,,
|
|
251
|
+
cmdbox-0.5.1.2.dist-info/LICENSE,sha256=sBzzPc5v-5LBuIFi2V4olsnoVg-3EBI0zRX5r19SOxE,1117
|
|
252
|
+
cmdbox-0.5.1.2.dist-info/METADATA,sha256=NhlyGdgD3Hp0HBPbEuknM_IRW4Y-uoeFkDmN6VMvmbM,24050
|
|
253
|
+
cmdbox-0.5.1.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
254
|
+
cmdbox-0.5.1.2.dist-info/entry_points.txt,sha256=PIoRz-tr503YwdMmd6nxuSn2dDltf4cUMVs98E9WgaA,48
|
|
255
|
+
cmdbox-0.5.1.2.dist-info/top_level.txt,sha256=eMEkD5jn8_0PkCAL8h5xJu4qAzF2O8Wf3vegFkKUXR4,7
|
|
256
|
+
cmdbox-0.5.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|