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.

@@ -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, 26)
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((e) => {
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
- option.text(`${cmd['title']}(mode=${cmd['mode']}, cmd=${cmd['cmd']})`);
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 text-muted">mode:<span class="cmd_mode"></span>, cmd:<span class="cmd_cmd"></span></h6>
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>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cmdbox
3
- Version: 0.5.1
3
+ Version: 0.5.1.2
4
4
  Summary: cmdbox: It is a command line application with a plugin mechanism.
5
5
  Home-page: https://github.com/hamacom2004jp/cmdbox
6
6
  Author: hamacom2004jp
@@ -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=EckA3XY1hLvl-wMEVk_JMlhiz4BQICd_f8H148-jZDU,1961
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=rwdRVQPwtDrW07f_07z6h090XwnSRDnwKxBUC-DTG4w,34688
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=mGsN4_ZZ8Lgdt6WyZAMe-yGG5DRPc_vXy9Hen9ruKU0,32801
199
- cmdbox/web/assets/cmdbox/list_pipe.js,sha256=mlzISyZUG90GbE0gxMzWpwZhzhojxqvft2gne6Vajco,11218
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=Cwr2Q21QZhC6iPg263joNQAKuANLhffiwsA0G25N_x0,24048
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,,