cmdbox 0.5.0.7__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cmdbox might be problematic. Click here for more details.

Files changed (51) hide show
  1. cmdbox/app/edge.py +96 -12
  2. cmdbox/app/features/cli/cmdbox_edge_config.py +9 -3
  3. cmdbox/app/features/cli/cmdbox_gui_start.py +1 -1
  4. cmdbox/app/features/cli/cmdbox_web_start.py +2 -1
  5. cmdbox/app/features/web/cmdbox_web_bbforce_cmd.py +1 -1
  6. cmdbox/app/features/web/cmdbox_web_copyright.py +1 -1
  7. cmdbox/app/features/web/cmdbox_web_del_cmd.py +1 -1
  8. cmdbox/app/features/web/cmdbox_web_del_pipe.py +1 -1
  9. cmdbox/app/features/web/cmdbox_web_do_signin.py +88 -32
  10. cmdbox/app/features/web/cmdbox_web_exec_cmd.py +2 -2
  11. cmdbox/app/features/web/cmdbox_web_exec_pipe.py +2 -2
  12. cmdbox/app/features/web/cmdbox_web_filer download.py +2 -3
  13. cmdbox/app/features/web/cmdbox_web_filer.py +1 -1
  14. cmdbox/app/features/web/cmdbox_web_filer_upload.py +1 -1
  15. cmdbox/app/features/web/cmdbox_web_get_cmd_choices.py +1 -1
  16. cmdbox/app/features/web/cmdbox_web_get_cmds.py +2 -2
  17. cmdbox/app/features/web/cmdbox_web_get_modes.py +2 -2
  18. cmdbox/app/features/web/cmdbox_web_get_server_opt.py +1 -1
  19. cmdbox/app/features/web/cmdbox_web_gui.py +2 -2
  20. cmdbox/app/features/web/cmdbox_web_list_cmd.py +2 -2
  21. cmdbox/app/features/web/cmdbox_web_list_pipe.py +2 -2
  22. cmdbox/app/features/web/cmdbox_web_load_cmd.py +1 -1
  23. cmdbox/app/features/web/cmdbox_web_load_pin.py +3 -5
  24. cmdbox/app/features/web/cmdbox_web_load_pipe.py +1 -1
  25. cmdbox/app/features/web/cmdbox_web_raw_cmd.py +1 -1
  26. cmdbox/app/features/web/cmdbox_web_raw_pipe.py +1 -1
  27. cmdbox/app/features/web/cmdbox_web_result.py +2 -2
  28. cmdbox/app/features/web/cmdbox_web_save_cmd.py +1 -1
  29. cmdbox/app/features/web/cmdbox_web_save_pin.py +2 -2
  30. cmdbox/app/features/web/cmdbox_web_save_pipe.py +1 -1
  31. cmdbox/app/features/web/cmdbox_web_signin.py +26 -8
  32. cmdbox/app/features/web/cmdbox_web_users.py +35 -37
  33. cmdbox/app/features/web/cmdbox_web_versions_cmdbox.py +1 -1
  34. cmdbox/app/features/web/cmdbox_web_versions_used.py +1 -1
  35. cmdbox/app/options.py +8 -8
  36. cmdbox/app/web.py +76 -555
  37. cmdbox/extensions/sample_project/sample/extensions/features.yml +38 -13
  38. cmdbox/extensions/sample_project/sample/extensions/user_list.yml +82 -40
  39. cmdbox/extensions/user_list.yml +10 -0
  40. cmdbox/version.py +2 -2
  41. cmdbox/web/assets/cmdbox/list_cmd.js +61 -6
  42. cmdbox/web/assets/cmdbox/signin.js +7 -0
  43. cmdbox/web/gui.html +12 -0
  44. cmdbox/web/signin.html +7 -0
  45. {cmdbox-0.5.0.7.dist-info → cmdbox-0.5.1.dist-info}/METADATA +1 -1
  46. {cmdbox-0.5.0.7.dist-info → cmdbox-0.5.1.dist-info}/RECORD +50 -51
  47. cmdbox/app/signin.py +0 -56
  48. {cmdbox-0.5.0.7.dist-info → cmdbox-0.5.1.dist-info}/LICENSE +0 -0
  49. {cmdbox-0.5.0.7.dist-info → cmdbox-0.5.1.dist-info}/WHEEL +0 -0
  50. {cmdbox-0.5.0.7.dist-info → cmdbox-0.5.1.dist-info}/entry_points.txt +0 -0
  51. {cmdbox-0.5.0.7.dist-info → cmdbox-0.5.1.dist-info}/top_level.txt +0 -0
cmdbox/app/edge.py CHANGED
@@ -149,6 +149,9 @@ class Edge(object):
149
149
  msg = dict(warn=f"Please set the numeric value in the oauth2_port. oauth2_port={opt['oauth2_port']}")
150
150
  return msg
151
151
  opt['oauth2_port'] = int(opt['oauth2_port'])
152
+ if opt['oauth2'] == 'azure' and ('oauth2_tenant_id' not in opt or opt['oauth2_tenant_id'] is None):
153
+ msg = dict(warn=f"Please run the `edge config` command. And please set the oauth2_tenant_id.")
154
+ return msg
152
155
  if opt['auth_type'] == 'oauth2' and ('oauth2_client_id' not in opt or opt['oauth2_client_id'] is None):
153
156
  msg = dict(warn=f"Please run the `edge config` command. And please set the oauth2_client_id.")
154
157
  return msg
@@ -181,7 +184,8 @@ class Edge(object):
181
184
  if self.svcert_no_verify:
182
185
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
183
186
  status, msg = self.signin(opt['auth_type'], opt['user'], opt['password'], opt['apikey'],
184
- opt['oauth2'], int(opt['oauth2_port']), opt['oauth2_client_id'], opt['oauth2_client_secret'],
187
+ opt['oauth2'], int(opt['oauth2_port']),
188
+ opt['oauth2_tenant_id'], opt['oauth2_client_id'], opt['oauth2_client_secret'],
185
189
  int(opt['oauth2_timeout']))
186
190
  if status != 0:
187
191
  return msg
@@ -244,7 +248,7 @@ class Edge(object):
244
248
  resq:queue.Queue = pipe_cmd['resq']
245
249
  del pipe_cmd['resq']
246
250
  tool = Tool(self.logger, self.appcls, self.ver)
247
- tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info)
251
+ tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info, self.oauth2)
248
252
  feat:feature.Feature = self.options.get_cmd_attr(pipe_cmd['mode'], pipe_cmd['cmd'], 'feature')
249
253
  while not thevent.is_set():
250
254
  prevres = None if prevq is None else prevq.get(pipe_cmd['timeout'])
@@ -301,7 +305,7 @@ class Edge(object):
301
305
  def mkcmd(opt):
302
306
  def _ex():
303
307
  tool = Tool(self.logger, self.appcls, self.ver)
304
- tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info)
308
+ tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info, self.oauth2)
305
309
  feat:feature.Feature = self.options.get_cmd_attr(opt['mode'], opt['cmd'], 'feature')
306
310
  for status, ret in feat.edgerun(opt, tool, self.logger, self.timeout):
307
311
  pass
@@ -348,7 +352,7 @@ class Edge(object):
348
352
  return status, json.loads(res)
349
353
 
350
354
  def signin(self, auth_type:str, user:str, password:str, apikey:str,
351
- oauth2:str, oauth2_port:int, oauth2_client_id:str, oauth2_client_secret:str,
355
+ oauth2:str, oauth2_port:int, oauth2_tenant_id:str, oauth2_client_id:str, oauth2_client_secret:str,
352
356
  oauth2_timeout:int) -> Tuple[int, Dict[str, Any]]:
353
357
  """
354
358
  サインインを行います
@@ -360,6 +364,7 @@ class Edge(object):
360
364
  apikey (str): APIキー
361
365
  oauth2 (str): OAuth2
362
366
  oauth2_port (int): OAuth2ポート
367
+ oauth2_tenant_id (str): OAuth2テナントID
363
368
  oauth2_client_id (str): OAuth2クライアントID
364
369
  oauth2_client_secret (str): OAuth2クライアントシークレット
365
370
  oauth2_timeout (int): OAuth2タイムアウト
@@ -369,13 +374,14 @@ class Edge(object):
369
374
  """
370
375
  self.session = requests.Session()
371
376
  self.signed_in = False
377
+ self.oauth2 = oauth2
372
378
  if auth_type == "noauth":
373
379
  status, res, _ = self.site_request(self.session.get, "/gui")
374
380
  if status != 0: return status, res
375
381
  status, self.user_info = self.load_user_info()
376
382
  self.user_info['auth_type'] = auth_type
377
383
  if status != 0: return status, res
378
- self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info)
384
+ self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info, self.oauth2)
379
385
  return 0, dict(success="No auth.")
380
386
 
381
387
  # ID/PW認証を使用する場合
@@ -392,7 +398,7 @@ class Edge(object):
392
398
  self.user_info['auth_type'] = auth_type
393
399
  self.user_info['password'] = password
394
400
  if status != 0: return status, res
395
- self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info)
401
+ self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info, self.oauth2)
396
402
  return 0, dict(success="Signin Success.")
397
403
 
398
404
  # APIKEY認証を使用する場合
@@ -407,7 +413,7 @@ class Edge(object):
407
413
  self.user_info['auth_type'] = auth_type
408
414
  self.user_info['apikey'] = apikey
409
415
  if status != 0: return status, res
410
- self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info)
416
+ self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info, self.oauth2)
411
417
  return 0, dict(success="Signin Success.")
412
418
 
413
419
  # OAuth2認証を使用する場合
@@ -449,7 +455,7 @@ class Edge(object):
449
455
  self.user_info['access_token'] = access_token
450
456
  if status != 0: return status, res
451
457
  self.signed_in = True
452
- self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info)
458
+ self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info, self.oauth2)
453
459
  return dict(success="Signin success. Please close your browser.")
454
460
  except Exception as e:
455
461
  raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
@@ -515,7 +521,7 @@ class Edge(object):
515
521
  self.user_info['access_token'] = access_token
516
522
  if status != 0: return status, res
517
523
  self.signed_in = True
518
- self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info)
524
+ self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info, self.oauth2)
519
525
  return dict(success="Signin success. Please close your browser.")
520
526
  except Exception as e:
521
527
  raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
@@ -543,6 +549,79 @@ class Edge(object):
543
549
  time.sleep(1)
544
550
  return 0, dict(success="Signin success.")
545
551
 
552
+
553
+ # Azure OAuth2を使用する場合
554
+ elif oauth2 == "azure":
555
+ if oauth2_tenant_id is None:
556
+ return 1, dict(warn="Please specify the --oauth2_tenant_id option.")
557
+ if oauth2_client_id is None:
558
+ return 1, dict(warn="Please specify the --oauth2_client_id option.")
559
+ if oauth2_client_secret is None:
560
+ return 1, dict(warn="Please specify the --oauth2_client_secret option.")
561
+ if oauth2_timeout is None:
562
+ return 1, dict(warn="Please specify the --oauth2_timeout option.")
563
+
564
+ redirect_uri = f'http://localhost:{oauth2_port}/oauth2/azure/callback'
565
+ # OAuth2認証のコールバックを受けるFastAPIサーバーを起動
566
+ fastapi = FastAPI()
567
+ @fastapi.get('/oauth2/azure/callback')
568
+ async def oauth2_azure_callback(req:Request):
569
+ if req.query_params['state'] != 'edge':
570
+ return dict(warn="Invalid state.")
571
+ # アクセストークン取得
572
+ headers = {'Content-Type': 'application/x-www-form-urlencoded',
573
+ 'Accept': 'application/json'}
574
+ data = {'tenant': oauth2_tenant_id,
575
+ 'code': req.query_params['code'],
576
+ 'scope': " ".join(['openid', 'profile', 'email']),
577
+ 'client_id': oauth2_client_id,
578
+ #'client_secret': oauth2_client_secret,
579
+ 'redirect_uri': redirect_uri,
580
+ 'grant_type': 'authorization_code'}
581
+ query = '&'.join([f'{k}={urllib.parse.quote(v)}' for k, v in data.items()])
582
+ try:
583
+ token_resp = self.session.post(url=f'https://login.microsoftonline.com/{oauth2_tenant_id}/oauth2/v2.0/token', headers=headers, data=query,
584
+ verify=not self.svcert_no_verify)
585
+ token_resp.raise_for_status()
586
+ token_json = token_resp.json()
587
+ access_token = token_json['access_token']
588
+ status, res, headers = self.site_request(self.session.get, f"/oauth2/azure/session/{access_token}/gui", ok_status=[200, 307])
589
+ if status != 0 or headers.get('signin') is None:
590
+ return dict(warn=f"Signin failed.")
591
+ status, self.user_info = self.load_user_info()
592
+ self.user_info['auth_type'] = auth_type
593
+ self.user_info['access_token'] = access_token
594
+ if status != 0: return status, res
595
+ self.signed_in = True
596
+ self.tool.set_session(self.session, self.svcert_no_verify, self.endpoint, self.icon_path, self.user_info, self.oauth2)
597
+ return dict(success="Signin success. Please close your browser.")
598
+ except Exception as e:
599
+ raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
600
+
601
+ if not hasattr(self, 'thUvicorn') or not self.thUvicorn.is_alive():
602
+ self.thUvicorn = web.ThreadedUvicorn(self.logger, Config(app=fastapi, host='localhost', port=oauth2_port), {})
603
+ self.thUvicorn.start()
604
+ time.sleep(1)
605
+
606
+ # OAuth2認証のリクエストを送信
607
+ data = {'scope': " ".join(['openid', 'profile', 'email']),
608
+ 'access_type': 'offline',
609
+ 'response_type': 'code',
610
+ 'redirect_uri': redirect_uri,
611
+ 'client_id': oauth2_client_id,
612
+ 'response_mode': 'query',
613
+ 'state': 'edge'}
614
+ query = '&'.join([f'{k}={urllib.parse.quote(v)}' for k, v in data.items()])
615
+ webbrowser.open(f'https://login.microsoftonline.com/{oauth2_tenant_id}/oauth2/v2.0/authorize?{query}')
616
+
617
+ # 認証完了まで指定秒数待つ
618
+ tm = time.time()
619
+ while not self.signed_in:
620
+ if time.time() - tm > oauth2_timeout:
621
+ return 1, dict(warn="Signin Timeout.")
622
+ time.sleep(1)
623
+ return 0, dict(success="Signin success.")
624
+
546
625
  return 1, dict(warn="unsupported auth_type.")
547
626
 
548
627
  class Tool(object):
@@ -578,7 +657,7 @@ class Tool(object):
578
657
  except Exception as e:
579
658
  self.logger.error(f"notify error. {e}", exc_info=True)
580
659
 
581
- def set_session(self, session:requests.Session, svcert_no_verify:bool, endpoint:str, icon_path:Path, user_info:Dict[str, Any]):
660
+ def set_session(self, session:requests.Session, svcert_no_verify:bool, endpoint:str, icon_path:Path, user_info:Dict[str, Any], oauth2:str):
582
661
  """
583
662
  セッションを設定します
584
663
 
@@ -588,12 +667,14 @@ class Tool(object):
588
667
  endpoint (str): エンドポイント
589
668
  icon_path (Path): アイコン画像のパス
590
669
  user_info (Dict[str, Any]): ユーザー情報
670
+ oauth2 (str): OAuth2
591
671
  """
592
672
  self.session = session
593
673
  self.svcert_no_verify = svcert_no_verify
594
674
  self.endpoint = endpoint
595
675
  self.icon_path = icon_path
596
676
  self.user = user_info
677
+ self.oauth2 = oauth2
597
678
 
598
679
  def exec_cmd(self, opt:Dict[str, Any], logger:logging.Logger, timeout:int, prevres:Any=None) -> Tuple[int, Dict[str, Any]]:
599
680
  """
@@ -694,9 +775,12 @@ class Tool(object):
694
775
  webbrowser.open(f"{self.endpoint}/dosignin_token/{token}{path}")
695
776
  return 0, dict(success="Open browser.")
696
777
  elif self.user['auth_type'] == "oauth2" and self.oauth2 == 'google':
697
- webbrowser.open(f"{self.endpoint}/oauth2/google/session/{self.user['access_token']}/{path}")
778
+ webbrowser.open(f"{self.endpoint}/oauth2/google/session/{self.user['access_token']}{path}")
698
779
  return 0, dict(success="Open browser.")
699
780
  elif self.user['auth_type'] == "oauth2" and self.oauth2 == 'github':
700
- webbrowser.open(f"{self.endpoint}/oauth2/github/session/{self.user['access_token']}/{path}")
781
+ webbrowser.open(f"{self.endpoint}/oauth2/github/session/{self.user['access_token']}{path}")
782
+ return 0, dict(success="Open browser.")
783
+ elif self.user['auth_type'] == "oauth2" and self.oauth2 == 'azure':
784
+ webbrowser.open(f"{self.endpoint}/oauth2/azure/session/{self.user['access_token']}{path}")
701
785
  return 0, dict(success="Open browser.")
702
786
  return 1, dict(warn="unsupported auth_type.")
@@ -49,7 +49,7 @@ class EdgeConfig(feature.UnsupportEdgeFeature):
49
49
  discription_en="Specifies the authentication method for endpoint connections.",
50
50
  choice_show=dict(idpw=["user","password"],
51
51
  apikey=["apikey"],
52
- oauth2=["oauth2","oauth2_port","oauth2_client_id","oauth2_client_secret"]),),
52
+ oauth2=["oauth2","oauth2_port"]),),
53
53
  dict(opt="user", type=Options.T_STR, default="user", required=False, multi=False, hide=False, choice=None,
54
54
  discription_ja="エンドポイントへの接続ユーザーを指定します。",
55
55
  discription_en="Specifies the user connecting to the endpoint."),
@@ -59,12 +59,18 @@ class EdgeConfig(feature.UnsupportEdgeFeature):
59
59
  dict(opt="apikey", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
60
60
  discription_ja="エンドポイントへの接続するためのAPIKEYを指定します。",
61
61
  discription_en="Specify the APIKEY to connect to the endpoint."),
62
- dict(opt="oauth2", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=["", "google", "github"],
62
+ dict(opt="oauth2", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=["", "google", "github", "azure"],
63
63
  discription_ja="OAuth2認証を使用してエンドポイントに接続します。",
64
- discription_en="Connect to the endpoint using OAuth2 authentication."),
64
+ discription_en="Connect to the endpoint using OAuth2 authentication.",
65
+ choice_show=dict(google=["oauth2_client_id","oauth2_client_secret"],
66
+ github=["oauth2_client_id","oauth2_client_secret"],
67
+ azure=["oauth2_tenant_id","oauth2_client_id","oauth2_client_secret"])),
65
68
  dict(opt="oauth2_port", type=Options.T_INT, default="8091", required=False, multi=False, hide=False, choice=None,
66
69
  discription_ja="OAuth2認証を使用する場合のコールバックポートを指定します。省略した時は `8091` を使用します。",
67
70
  discription_en="Specifies the callback port when OAuth2 authentication is used. If omitted, `8091` is used."),
71
+ dict(opt="oauth2_tenant_id", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
72
+ discription_ja="OAuth2認証を使用するときのテナントIDを指定します。",
73
+ discription_en="Specifies the tenant ID when OAuth2 authentication is used."),
68
74
  dict(opt="oauth2_client_id", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
69
75
  discription_ja="OAuth2認証を使用するときのクライアントIDを指定します。",
70
76
  discription_en="Specifies the client ID when OAuth2 authentication is used."),
@@ -86,7 +86,7 @@ class GuiStart(feature.UnsupportEdgeFeature):
86
86
  dict(opt="session_secure", type=Options.T_BOOL, default=False, required=False, multi=False, hide=True, choice=[True, False],
87
87
  discription_ja="サインインしたユーザーのセッションにSecureフラグを設定します。",
88
88
  discription_en="Set the Secure flag for the signed-in user's session."),
89
- dict(opt="session_timeout", type=Options.T_INT, default="600", required=False, multi=False, hide=True, choice=None,
89
+ dict(opt="session_timeout", type=Options.T_INT, default="900", required=False, multi=False, hide=True, choice=None,
90
90
  discription_ja="サインインしたユーザーのセッションタイムアウトの時間を秒で指定します。",
91
91
  discription_en="Specify the session timeout in seconds for signed-in users."),
92
92
  dict(opt="guvicorn_workers", type=Options.T_INT, default=multiprocessing.cpu_count()*2, required=False, multi=False, hide=True, choice=None,
@@ -86,7 +86,7 @@ class WebStart(feature.UnsupportEdgeFeature):
86
86
  dict(opt="session_secure", type=Options.T_BOOL, default=False, required=False, multi=False, hide=True, choice=[True, False],
87
87
  discription_ja="サインインしたユーザーのセッションにSecureフラグを設定します。",
88
88
  discription_en="Set the Secure flag for the signed-in user's session."),
89
- dict(opt="session_timeout", type=Options.T_INT, default="600", required=False, multi=False, hide=True, choice=None,
89
+ dict(opt="session_timeout", type=Options.T_INT, default="900", required=False, multi=False, hide=True, choice=None,
90
90
  discription_ja="サインインしたユーザーのセッションタイムアウトの時間を秒で指定します。",
91
91
  discription_en="Specify the session timeout in seconds for signed-in users."),
92
92
  dict(opt="guvicorn_workers", type=Options.T_INT, default=multiprocessing.cpu_count()*2, required=False, multi=False, hide=True, choice=None,
@@ -171,6 +171,7 @@ class WebStart(feature.UnsupportEdgeFeature):
171
171
  common.print_format(msg, args.format, tm, args.output_json, args.output_json_append, pf=pf)
172
172
  return 0, msg, w
173
173
  except Exception as e:
174
+ logger.error(f"Web server start error. {e}", exc_info=True)
174
175
  msg = dict(warn=f"Web server start error. {e}")
175
176
  common.print_format(msg, args.format, tm, args.output_json, args.output_json_append, pf=pf)
176
177
  return 1, msg, w
@@ -15,7 +15,7 @@ class BbforceCmd(feature.WebFeature):
15
15
  """
16
16
  @app.get('/bbforce_cmd')
17
17
  async def del_cmd(req:Request, res:Response):
18
- signin = web.check_signin(req, res)
18
+ signin = web.signin.check_signin(req, res)
19
19
  if signin is not None:
20
20
  raise HTTPException(status_code=401, detail=self.DEFAULT_401_MESSAGE)
21
21
  if web.logger.level == logging.DEBUG:
@@ -15,7 +15,7 @@ class Copyright(feature.WebFeature):
15
15
  """
16
16
  @app.get('/copyright', response_class=PlainTextResponse)
17
17
  async def copyright(req:Request, res:Response):
18
- signin = web.check_signin(req, res)
18
+ signin = web.signin.check_signin(req, res)
19
19
  if signin is not None:
20
20
  raise HTTPException(status_code=401, detail=self.DEFAULT_401_MESSAGE)
21
21
  return self.ver.__copyright__
@@ -14,7 +14,7 @@ class DelCmd(feature.WebFeature):
14
14
  """
15
15
  @app.post('/gui/del_cmd')
16
16
  async def del_cmd(req:Request, res:Response):
17
- signin = web.check_signin(req, res)
17
+ signin = web.signin.check_signin(req, res)
18
18
  if signin is not None:
19
19
  raise HTTPException(status_code=401, detail=self.DEFAULT_401_MESSAGE)
20
20
  form = await req.form()
@@ -14,7 +14,7 @@ class DelPipe(feature.WebFeature):
14
14
  """
15
15
  @app.post('/gui/del_pipe')
16
16
  async def del_pipe(req:Request, res:Response):
17
- signin = web.check_signin(req, res)
17
+ signin = web.signin.check_signin(req, res)
18
18
  if signin is not None:
19
19
  raise HTTPException(status_code=401, detail=self.DEFAULT_401_MESSAGE)
20
20
  form = await req.form()
@@ -1,4 +1,5 @@
1
- from cmdbox.app import common, signin
1
+ from cmdbox.app import common
2
+ from cmdbox.app.auth.signin import Signin
2
3
  from cmdbox.app.commons import convert
3
4
  from cmdbox.app.features.web import cmdbox_web_signin
4
5
  from cmdbox.app.web import Web
@@ -33,6 +34,7 @@ class DoSignin(cmdbox_web_signin.Signin):
33
34
  name = form.get('name')
34
35
  passwd = form.get('password')
35
36
  # edgeからtokenによる認証の場合
37
+ signin_data = web.signin.get_data()
36
38
  token_ok = False
37
39
  if token is not None:
38
40
  if web.logger.level == logging.DEBUG:
@@ -40,7 +42,7 @@ class DoSignin(cmdbox_web_signin.Signin):
40
42
  token = convert.b64str2str(token)
41
43
  token = json.loads(token)
42
44
  name = token['user']
43
- user = [u for u in web.signin_file_data['users'] if u['name'] == name]
45
+ user = [u for u in signin_data['users'] if u['name'] == name]
44
46
  if len(user) <= 0:
45
47
  raise HTTPException(status_code=401, detail='Unauthorized')
46
48
  user = user[0]
@@ -57,7 +59,7 @@ class DoSignin(cmdbox_web_signin.Signin):
57
59
  if not token_ok:
58
60
  if name == '' or passwd == '':
59
61
  return RedirectResponse(url=f'/signin/{next}?error=1')
60
- user = [u for u in web.signin_file_data['users'] if u['name'] == name and u['hash'] != 'oauth2']
62
+ user = [u for u in signin_data['users'] if u['name'] == name and u['hash'] != 'oauth2']
61
63
  if len(user) <= 0:
62
64
  return RedirectResponse(url=f'/signin/{next}?error=1')
63
65
  user = user[0]
@@ -67,9 +69,9 @@ class DoSignin(cmdbox_web_signin.Signin):
67
69
  # ロックアウトチェック
68
70
  pass_miss_count = web.user_data(None, uid, name, 'password', 'pass_miss_count')
69
71
  pass_miss_count = 0 if pass_miss_count is None else int(pass_miss_count)
70
- if 'password' in web.signin_file_data and web.signin_file_data['password']['lockout']['enabled']:
71
- threshold = web.signin_file_data['password']['lockout']['threshold']
72
- reset = web.signin_file_data['password']['lockout']['reset']
72
+ if 'password' in signin_data and signin_data['password']['lockout']['enabled']:
73
+ threshold = signin_data['password']['lockout']['threshold']
74
+ reset = signin_data['password']['lockout']['reset']
73
75
  pass_miss_last = web.user_data(None, uid, name, 'password', 'pass_miss_last')
74
76
  if pass_miss_last is None:
75
77
  pass_miss_last = web.user_data(None, uid, name, 'password', 'pass_miss_last', datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'))
@@ -95,17 +97,17 @@ class DoSignin(cmdbox_web_signin.Signin):
95
97
  web.user_data(None, uid, name, 'password', 'pass_miss_count', pass_miss_count+1)
96
98
  web.logger.warning(f'Failed to signin. name={name}, pass_miss_count={pass_miss_count+1}')
97
99
  return RedirectResponse(url=f'/signin/{next}?error=1')
98
- group_names = list(set(web.correct_group(user['groups'])))
99
- gids = [g['gid'] for g in web.signin_file_data['groups'] if g['name'] in group_names]
100
+ group_names = list(set(web.signin.__class__.correct_group(signin_data, user['groups'], None)))
101
+ gids = [g['gid'] for g in signin_data['groups'] if g['name'] in group_names]
100
102
  email = user.get('email', '')
101
103
  # パスワード最終更新日時取得
102
104
  last_update = web.user_data(None, uid, name, 'password', 'last_update')
103
105
  notify_passchange = True if last_update is None else False
104
106
  # パスワード認証の場合はパスワード有効期限チェック
105
- if user['hash']!='oauth2' and 'password' in web.signin_file_data and not notify_passchange:
107
+ if user['hash']!='oauth2' and 'password' in signin_data and not notify_passchange:
106
108
  last_update = datetime.datetime.strptime(last_update, '%Y-%m-%dT%H:%M:%S')
107
109
  # パスワード有効期限
108
- expiration = web.signin_file_data['password']['expiration']
110
+ expiration = signin_data['password']['expiration']
109
111
  if expiration['enabled']:
110
112
  period = expiration['period']
111
113
  notify = expiration['notify']
@@ -122,7 +124,7 @@ class DoSignin(cmdbox_web_signin.Signin):
122
124
  next = f"../{next}" if token_ok else next
123
125
  return RedirectResponse(url=f'../{next}{"?warn=passchange" if notify_passchange else ""}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
124
126
 
125
- def _load_signin(signin_module:str, appcls, ver):
127
+ def _load_signin(web:Web, signin_module:str, appcls, ver):
126
128
  """
127
129
  サインインオブジェクトを読込む
128
130
 
@@ -138,26 +140,31 @@ class DoSignin(cmdbox_web_signin.Signin):
138
140
  try:
139
141
  mod = importlib.import_module(signin_module)
140
142
  members = inspect.getmembers(mod, inspect.isclass)
143
+ signin_data = web.signin.get_data()
141
144
  for name, cls in members:
142
- if cls is not signin.Signin or not issubclass(cls, signin.Signin):
143
- continue
144
- sobj = cls(appcls, ver)
145
- return sobj
145
+ if cls is Signin or issubclass(cls, Signin):
146
+ sobj = cls(web.logger, web.signin_file, signin_data, appcls, ver)
147
+ return sobj
146
148
  return None
147
149
  except Exception as e:
148
150
  web.logger.error(f'Failed to load signin. {e}', exc_info=True)
149
151
  raise e
150
152
 
151
- self.google_signin = signin.Signin(app, web.ver)
152
- self.github_signin = signin.Signin(app, web.ver)
153
- if web.signin_file_data is not None:
153
+ signin_data = web.signin.get_data()
154
+ self.google_signin = Signin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
155
+ self.github_signin = Signin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
156
+ self.azure_signin = Signin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
157
+ if signin_data is not None:
154
158
  # signinオブジェクトの指定があった場合読込む
155
- if 'signin_module' in web.signin_file_data['oauth2']['providers']['google']:
156
- sobj = _load_signin(web.signin_file_data['oauth2']['providers']['google']['signin_module'], self.appcls, self.ver)
159
+ if 'signin_module' in signin_data['oauth2']['providers']['google']:
160
+ sobj = _load_signin(web, signin_data['oauth2']['providers']['google']['signin_module'], self.appcls, self.ver)
157
161
  self.google_signin = sobj if sobj is not None else self.google_signin
158
- if 'signin_module' in web.signin_file_data['oauth2']['providers']['google']:
159
- sobj = _load_signin(web.signin_file_data['oauth2']['providers']['github']['signin_module'], self.appcls, self.ver)
162
+ if 'signin_module' in signin_data['oauth2']['providers']['github']:
163
+ sobj = _load_signin(web, signin_data['oauth2']['providers']['github']['signin_module'], self.appcls, self.ver)
160
164
  self.github_signin = sobj if sobj is not None else self.github_signin
165
+ if 'signin_module' in signin_data['oauth2']['providers']['azure']:
166
+ sobj = _load_signin(web, signin_data['oauth2']['providers']['azure']['signin_module'], self.appcls, self.ver)
167
+ self.azure_signin = sobj if sobj is not None else self.azure_signin
161
168
 
162
169
  def _set_session(req:Request, user:dict, email:str, hashed_password:str, access_token:str, group_names:list, gids:list):
163
170
  """
@@ -191,7 +198,7 @@ class DoSignin(cmdbox_web_signin.Signin):
191
198
 
192
199
  @app.get('/oauth2/google/callback')
193
200
  async def oauth2_google_callback(req:Request, res:Response):
194
- conf = web.signin_file_data['oauth2']['providers']['google']
201
+ conf = web.signin.get_data()['oauth2']['providers']['google']
195
202
  headers = {'Content-Type': 'application/x-www-form-urlencoded'}
196
203
  next = req.query_params['state']
197
204
  data = {'code': req.query_params['code'],
@@ -206,7 +213,7 @@ class DoSignin(cmdbox_web_signin.Signin):
206
213
  token_resp.raise_for_status()
207
214
  token_json = token_resp.json()
208
215
  access_token = token_json['access_token']
209
- return await oauth2_google_session(next, access_token, req, res)
216
+ return await oauth2_google_session(access_token, next, req, res)
210
217
  except Exception as e:
211
218
  web.logger.warning(f'Failed to get token. {e}', exc_info=True)
212
219
  raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
@@ -223,12 +230,11 @@ class DoSignin(cmdbox_web_signin.Signin):
223
230
  user_info_json = user_info_resp.json()
224
231
  email = user_info_json['email']
225
232
  # サインイン判定
226
- copy_signin_data = copy.deepcopy(web.signin_file_data)
227
- jadge, user = self.google_signin.jadge(access_token, email, copy_signin_data)
233
+ jadge, user = self.google_signin.jadge(access_token, email)
228
234
  if not jadge:
229
235
  return RedirectResponse(url=f'/signin/{next}?error=appdeny')
230
236
  # グループ取得
231
- group_names, gids = self.google_signin.get_groups(access_token, user, copy_signin_data)
237
+ group_names, gids = self.google_signin.get_groups(access_token, user)
232
238
  # セッションに保存
233
239
  _set_session(req, user, email, None, access_token, group_names, gids)
234
240
  return RedirectResponse(url=f'../../{next}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
@@ -238,7 +244,7 @@ class DoSignin(cmdbox_web_signin.Signin):
238
244
 
239
245
  @app.get('/oauth2/github/callback')
240
246
  async def oauth2_github_callback(req:Request, res:Response):
241
- conf = web.signin_file_data['oauth2']['providers']['github']
247
+ conf = web.signin.get_data()['oauth2']['providers']['github']
242
248
  headers = {'Content-Type': 'application/x-www-form-urlencoded',
243
249
  'Accept': 'application/json'}
244
250
  next = req.query_params['state']
@@ -253,7 +259,7 @@ class DoSignin(cmdbox_web_signin.Signin):
253
259
  token_resp.raise_for_status()
254
260
  token_json = token_resp.json()
255
261
  access_token = token_json['access_token']
256
- return await oauth2_github_session(next, access_token, req, res)
262
+ return await oauth2_github_session(access_token, next, req, res)
257
263
  except Exception as e:
258
264
  web.logger.warning(f'Failed to get token. {e}', exc_info=True)
259
265
  raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
@@ -275,12 +281,62 @@ class DoSignin(cmdbox_web_signin.Signin):
275
281
  email = u['email']
276
282
  break
277
283
  # サインイン判定
278
- copy_signin_data = copy.deepcopy(web.signin_file_data)
279
- jadge, user = self.github_signin.jadge(access_token, email, copy_signin_data)
284
+ jadge, user = self.github_signin.jadge(access_token, email)
280
285
  if not jadge:
281
286
  return RedirectResponse(url=f'/signin/{next}?error=appdeny')
282
287
  # グループ取得
283
- group_names, gids = self.github_signin.get_groups(access_token, user, copy_signin_data)
288
+ group_names, gids = self.github_signin.get_groups(access_token, user)
289
+ # セッションに保存
290
+ _set_session(req, user, email, None, access_token, group_names, gids)
291
+ return RedirectResponse(url=f'../../{next}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
292
+ except Exception as e:
293
+ web.logger.warning(f'Failed to get token. {e}', exc_info=True)
294
+ raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
295
+
296
+ @app.get('/oauth2/azure/callback')
297
+ async def oauth2_azure_callback(req:Request, res:Response):
298
+ conf = web.signin.get_data()['oauth2']['providers']['azure']
299
+ headers = {'Content-Type': 'application/x-www-form-urlencoded',
300
+ 'Accept': 'application/json'}
301
+ next = req.query_params['state']
302
+ data = {'tenant': conf['tenant_id'],
303
+ 'code': req.query_params['code'],
304
+ 'scope': " ".join(conf['scope']),
305
+ 'client_id': conf['client_id'],
306
+ #'client_secret': conf['client_secret'],
307
+ 'redirect_uri': conf['redirect_uri'],
308
+ 'grant_type': 'authorization_code'}
309
+ query = '&'.join([f'{k}={urllib.parse.quote(v)}' for k, v in data.items()])
310
+ try:
311
+ # アクセストークン取得
312
+ token_resp = requests.post(url=f'https://login.microsoftonline.com/{conf["tenant_id"]}/oauth2/v2.0/token', headers=headers, data=query)
313
+ token_resp.raise_for_status()
314
+ token_json = token_resp.json()
315
+ access_token = token_json['access_token']
316
+ return await oauth2_azure_session(access_token, next, req, res)
317
+ except Exception as e:
318
+ web.logger.warning(f'Failed to get token. {e}', exc_info=True)
319
+ raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
320
+
321
+ @app.get('/oauth2/azure/session/{access_token}/{next}')
322
+ async def oauth2_azure_session(access_token:str, next:str, req:Request, res:Response):
323
+ try:
324
+ # ユーザー情報取得(email)
325
+ user_info_resp = requests.get(
326
+ url='https://graph.microsoft.com/v1.0/me',
327
+ #url='https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$Top=999',
328
+ headers={'Authorization': f'Bearer {access_token}'}
329
+ )
330
+ user_info_resp.raise_for_status()
331
+ user_info_json = user_info_resp.json()
332
+ if isinstance(user_info_json, dict):
333
+ email = user_info_json.get('mail', 'notfound')
334
+ # サインイン判定
335
+ jadge, user = self.azure_signin.jadge(access_token, email)
336
+ if not jadge:
337
+ return RedirectResponse(url=f'/signin/{next}?error=appdeny')
338
+ # グループ取得
339
+ group_names, gids = self.azure_signin.get_groups(access_token, user)
284
340
  # セッションに保存
285
341
  _set_session(req, user, email, None, access_token, group_names, gids)
286
342
  return RedirectResponse(url=f'../../{next}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
@@ -26,7 +26,7 @@ class ExecCmd(cmdbox_web_load_cmd.LoadCmd):
26
26
  @app.post('/exec_cmd/{title}')
27
27
  async def exec_cmd(req:Request, res:Response, title:str=None):
28
28
  try:
29
- signin = web.check_signin(req, res)
29
+ signin = web.signin.check_signin(req, res)
30
30
  if signin is not None:
31
31
  raise HTTPException(status_code=401, detail=self.DEFAULT_401_MESSAGE)
32
32
  opt = None
@@ -114,7 +114,7 @@ class ExecCmd(cmdbox_web_load_cmd.LoadCmd):
114
114
  appcls = app.CmdBoxApp if appcls is None else appcls
115
115
  web.container['cmdbox_app'] = ap = appcls.getInstance(appcls=appcls, ver=self.ver)
116
116
  if 'mode' in opt and 'cmd' in opt:
117
- if not web.check_cmd(req, res, opt['mode'], opt['cmd']):
117
+ if not web.signin.check_cmd(req, res, opt['mode'], opt['cmd']):
118
118
  return dict(warn=f'Command "{title}" failed. Execute command denyed. mode={opt["mode"]}, cmd={opt["cmd"]}')
119
119
  if 'host' in opt: opt['host'] = web.redis_host
120
120
  if 'port' in opt: opt['port'] = web.redis_port
@@ -29,7 +29,7 @@ class ExecPipe(cmdbox_web_load_pipe.LoadPipe, cmdbox_web_raw_pipe.RawPipe):
29
29
  async def exec_pipe(req:Request, res:Response, title:str=None):
30
30
  upfiles = dict()
31
31
  try:
32
- signin = web.check_signin(req, res)
32
+ signin = web.signin.check_signin(req, res)
33
33
  if signin is not None:
34
34
  raise HTTPException(status_code=401, detail=self.DEFAULT_401_MESSAGE)
35
35
  opt = None
@@ -71,7 +71,7 @@ class ExecPipe(cmdbox_web_load_pipe.LoadPipe, cmdbox_web_raw_pipe.RawPipe):
71
71
  msg = f'Command "{cmd_title}" failed. This command is not available in web mode.'
72
72
  self.callback_return_pipe_exec_func(web, title, dict(warn=msg))
73
73
  raise HTTPException(401, detail=msg)
74
- if not web.check_cmd(req, res, cmd_opt['mode'], cmd_opt['cmd']):
74
+ if not web.signin.check_cmd(req, res, cmd_opt['mode'], cmd_opt['cmd']):
75
75
  msg = f'Command "{cmd_title}" failed. Execute command denyed. mode={cmd_opt["mode"]}, cmd={cmd_opt["cmd"]}'
76
76
  self.callback_return_pipe_exec_func(web, title, dict(warn=msg))
77
77
  raise HTTPException(401, detail=msg)
@@ -3,9 +3,8 @@ from cmdbox.app.commons import convert
3
3
  from cmdbox.app.features.web import cmdbox_web_exec_cmd
4
4
  from cmdbox.app.web import Web
5
5
  from fastapi import FastAPI, Request, Response, HTTPException
6
- from fastapi.responses import HTMLResponse, StreamingResponse
6
+ from fastapi.responses import StreamingResponse
7
7
  from pathlib import Path
8
- from typing import Dict, Any
9
8
  import io
10
9
  import urllib.parse
11
10
 
@@ -21,7 +20,7 @@ class FilerDownload(cmdbox_web_exec_cmd.ExecCmd):
21
20
  """
22
21
  @app.get('/filer/download/{constr:str}', response_class=StreamingResponse)
23
22
  async def filer_download(constr:str, req:Request, res:Response):
24
- signin = web.check_signin(req, res)
23
+ signin = web.signin.check_signin(req, res)
25
24
  if signin is not None:
26
25
  raise HTTPException(status_code=401, detail=self.DEFAULT_401_MESSAGE)
27
26
  try:
@@ -23,7 +23,7 @@ class Filer(feature.WebFeature):
23
23
  @app.get('/filer', response_class=HTMLResponse)
24
24
  @app.post('/filer', response_class=HTMLResponse)
25
25
  async def filer(req:Request, res:Response):
26
- signin = web.check_signin(req, res)
26
+ signin = web.signin.check_signin(req, res)
27
27
  if signin is not None:
28
28
  return signin
29
29
  res.headers['Access-Control-Allow-Origin'] = '*'