cmdbox 0.5.3.1__py3-none-any.whl → 0.5.4__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 (89) hide show
  1. cmdbox/app/auth/__init__.py +0 -0
  2. cmdbox/app/auth/azure_signin.py +38 -0
  3. cmdbox/app/auth/azure_signin_saml.py +12 -0
  4. cmdbox/app/auth/github_signin.py +38 -0
  5. cmdbox/app/auth/google_signin.py +32 -0
  6. cmdbox/app/auth/signin.py +47 -4
  7. cmdbox/app/auth/signin_saml.py +61 -0
  8. cmdbox/app/edge.py +198 -61
  9. cmdbox/app/feature.py +0 -1
  10. cmdbox/app/features/cli/cmdbox_edge_config.py +19 -5
  11. cmdbox/app/features/cli/cmdbox_web_user_add.py +3 -3
  12. cmdbox/app/features/cli/cmdbox_web_user_edit.py +3 -3
  13. cmdbox/app/features/web/cmdbox_web_do_signin.py +79 -103
  14. cmdbox/app/features/web/cmdbox_web_signin.py +23 -1
  15. cmdbox/app/web.py +13 -12
  16. cmdbox/extensions/sample_project/sample/extensions/features.yml +23 -0
  17. cmdbox/extensions/sample_project/sample/extensions/user_list.yml +40 -6
  18. cmdbox/extensions/user_list.yml +36 -6
  19. cmdbox/licenses/{LICENSE.python-multipart.0.0.17(Apache Software License).txt → LICENSE.async-timeout.5.0.1(Apache Software License).txt } +2 -3
  20. cmdbox/licenses/files.txt +10 -9
  21. cmdbox/version.py +2 -2
  22. cmdbox/web/assets/cmdbox/signin.js +13 -0
  23. cmdbox/web/assets/cmdbox/users.js +1 -1
  24. cmdbox/web/signin.html +10 -6
  25. {cmdbox-0.5.3.1.dist-info → cmdbox-0.5.4.dist-info}/METADATA +64 -10
  26. {cmdbox-0.5.3.1.dist-info → cmdbox-0.5.4.dist-info}/RECORD +39 -83
  27. cmdbox/app/features/web/cmdbox_web_load_pin.py +0 -43
  28. cmdbox/app/features/web/cmdbox_web_save_pin.py +0 -42
  29. cmdbox/licenses/LICENSE.Jinja2.3.1.4(BSD License).txt +0 -28
  30. cmdbox/licenses/LICENSE.Pygments.2.18.0(BSD License).txt +0 -25
  31. cmdbox/licenses/LICENSE.Sphinx.8.1.3(BSD License).txt +0 -31
  32. cmdbox/licenses/LICENSE.anyio.4.6.2.post1(MIT License).txt +0 -20
  33. cmdbox/licenses/LICENSE.argcomplete.3.5.1(Apache Software License).txt +0 -177
  34. cmdbox/licenses/LICENSE.argcomplete.3.6.1(Apache Software License).txt +0 -177
  35. cmdbox/licenses/LICENSE.babel.2.16.0(BSD License).txt +0 -27
  36. cmdbox/licenses/LICENSE.certifi.2025.1.31(Mozilla Public License 2.0 (MPL 2.0)).txt +0 -20
  37. cmdbox/licenses/LICENSE.charset-normalizer.3.4.0(MIT License).txt +0 -21
  38. cmdbox/licenses/LICENSE.click.8.1.7(BSD License).txt +0 -28
  39. cmdbox/licenses/LICENSE.cryptography.43.0.3(Apache Software License; BSD License).txt +0 -3
  40. cmdbox/licenses/LICENSE.fastapi.0.115.5(MIT License).txt +0 -21
  41. cmdbox/licenses/LICENSE.gevent.25.4.1(MIT).txt +0 -25
  42. cmdbox/licenses/LICENSE.greenlet.3.2.0(MIT AND Python-2.0).txt +0 -30
  43. cmdbox/licenses/LICENSE.importlib_metadata.8.6.1(Apache Software License).txt +0 -202
  44. cmdbox/licenses/LICENSE.keyring.25.5.0(MIT License).txt +0 -17
  45. cmdbox/licenses/LICENSE.more-itertools.10.6.0(MIT License).txt +0 -19
  46. cmdbox/licenses/LICENSE.nh3.0.2.18(MIT).txt +0 -1
  47. cmdbox/licenses/LICENSE.numpy.2.2.4(BSD License).txt +0 -950
  48. cmdbox/licenses/LICENSE.pillow.11.0.0(CMU License (MIT-CMU)).txt +0 -1226
  49. cmdbox/licenses/LICENSE.pillow.11.1.0(CMU License (MIT-CMU)).txt +0 -1213
  50. cmdbox/licenses/LICENSE.pkginfo.1.10.0(MIT License).txt +0 -21
  51. cmdbox/licenses/LICENSE.prettytable.3.12.0(BSD License).txt +0 -30
  52. cmdbox/licenses/LICENSE.prompt_toolkit.3.0.50(BSD License).txt +0 -27
  53. cmdbox/licenses/LICENSE.psycopg-pool.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +0 -165
  54. cmdbox/licenses/LICENSE.pydantic.2.10.2(MIT License).txt +0 -21
  55. cmdbox/licenses/LICENSE.pydantic.2.11.1(MIT License).txt +0 -21
  56. cmdbox/licenses/LICENSE.pydantic_core.2.27.1(MIT License).txt +0 -21
  57. cmdbox/licenses/LICENSE.pydantic_core.2.33.0(MIT License).txt +0 -21
  58. cmdbox/licenses/LICENSE.python-dotenv.1.0.1(BSD License).txt +0 -27
  59. cmdbox/licenses/LICENSE.redis.5.2.0(MIT License).txt +0 -21
  60. cmdbox/licenses/LICENSE.rich.13.9.4(MIT License).txt +0 -19
  61. cmdbox/licenses/LICENSE.six.1.16.0(MIT License).txt +0 -18
  62. cmdbox/licenses/LICENSE.sphinx-intl.2.3.0(BSD License).txt +0 -25
  63. cmdbox/licenses/LICENSE.starlette.0.41.3(BSD License).txt +0 -27
  64. cmdbox/licenses/LICENSE.starlette.0.46.1(BSD License).txt +0 -27
  65. cmdbox/licenses/LICENSE.tomli.2.1.0(MIT License).txt +0 -21
  66. cmdbox/licenses/LICENSE.twine.5.1.1(Apache Software License).txt +0 -174
  67. cmdbox/licenses/LICENSE.typing_extensions.4.12.2(Python Software Foundation License).txt +0 -279
  68. cmdbox/licenses/LICENSE.typing_extensions.4.13.0(UNKNOWN).txt +0 -279
  69. cmdbox/licenses/LICENSE.urllib3.2.2.3(MIT License).txt +0 -21
  70. cmdbox/licenses/LICENSE.urllib3.2.3.0(MIT License).txt +0 -21
  71. cmdbox/licenses/LICENSE.uvicorn.0.34.0(BSD License).txt +0 -27
  72. cmdbox/licenses/LICENSE.uvicorn.0.34.1(BSD License).txt +0 -27
  73. cmdbox/licenses/LICENSE.watchfiles.1.0.0(MIT License).txt +0 -21
  74. cmdbox/licenses/LICENSE.watchfiles.1.0.4(MIT License).txt +0 -21
  75. cmdbox/licenses/LICENSE.websockets.14.1(BSD License).txt +0 -24
  76. cmdbox/licenses/LICENSE.zope.interface.7.1.1(Zope Public License).txt +0 -44
  77. /cmdbox/licenses/{LICENSE.certifi.2024.8.30(Mozilla Public License 2.0 (MPL 2.0)).txt → LICENSE.certifi.2025.4.26(Mozilla Public License 2.0 (MPL 2.0)).txt} +0 -0
  78. /cmdbox/licenses/{LICENSE.gevent.24.11.1(MIT License).txt → LICENSE.gevent.25.4.2(MIT).txt} +0 -0
  79. /cmdbox/licenses/{LICENSE.greenlet.3.1.1(MIT License).txt → LICENSE.greenlet.3.2.1(MIT AND Python-2.0).txt} +0 -0
  80. /cmdbox/licenses/{LICENSE.h11.0.14.0(MIT License).txt → LICENSE.h11.0.16.0(MIT License).txt} +0 -0
  81. /cmdbox/licenses/{LICENSE.importlib_metadata.8.5.0(Apache Software License).txt → LICENSE.importlib_metadata.8.7.0(Apache Software License).txt} +0 -0
  82. /cmdbox/licenses/{LICENSE.more-itertools.10.5.0(MIT License).txt → LICENSE.more-itertools.10.7.0(MIT License).txt} +0 -0
  83. /cmdbox/licenses/{LICENSE.numpy.2.1.3(BSD License).txt → LICENSE.numpy.2.2.5(BSD License).txt} +0 -0
  84. /cmdbox/licenses/{LICENSE.packaging.24.2(Apache Software License; BSD License).txt → LICENSE.packaging.25.0(Apache Software License; BSD License).txt} +0 -0
  85. /cmdbox/licenses/{LICENSE.uvicorn.0.32.1(BSD License).txt → LICENSE.uvicorn.0.34.2(BSD License).txt} +0 -0
  86. {cmdbox-0.5.3.1.dist-info → cmdbox-0.5.4.dist-info}/LICENSE +0 -0
  87. {cmdbox-0.5.3.1.dist-info → cmdbox-0.5.4.dist-info}/WHEEL +0 -0
  88. {cmdbox-0.5.3.1.dist-info → cmdbox-0.5.4.dist-info}/entry_points.txt +0 -0
  89. {cmdbox-0.5.3.1.dist-info → cmdbox-0.5.4.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,19 @@
1
+ import urllib.parse
1
2
  from cmdbox.app import common
2
- from cmdbox.app.auth.signin import Signin
3
+ from cmdbox.app.auth import signin, signin_saml, azure_signin, azure_signin_saml, github_signin, google_signin
3
4
  from cmdbox.app.commons import convert
4
5
  from cmdbox.app.features.web import cmdbox_web_signin
5
6
  from cmdbox.app.web import Web
6
7
  from fastapi import FastAPI, Request, Response, HTTPException
7
8
  from fastapi.responses import HTMLResponse, RedirectResponse
9
+ from typing import Any, Dict
8
10
  import copy
9
11
  import datetime
10
12
  import importlib
11
13
  import inspect
12
14
  import json
13
15
  import logging
14
- import requests
15
- import urllib.parse
16
+ import urllib
16
17
 
17
18
 
18
19
  class DoSignin(cmdbox_web_signin.Signin):
@@ -60,7 +61,7 @@ class DoSignin(cmdbox_web_signin.Signin):
60
61
  if name == '' or passwd == '':
61
62
  web.options.audit_exec(req, res, web, body=dict(msg='signin failed.'), audit_type='auth')
62
63
  return RedirectResponse(url=f'/signin/{next}?error=1')
63
- user = [u for u in signin_data['users'] if u['name'] == name and u['hash'] != 'oauth2']
64
+ user = [u for u in signin_data['users'] if u['name'] == name and u['hash'] != 'oauth2' and u['hash'] != 'saml']
64
65
  if len(user) <= 0:
65
66
  web.options.audit_exec(req, res, web, body=dict(msg='signin failed.'), audit_type='auth')
66
67
  return RedirectResponse(url=f'/signin/{next}?error=1')
@@ -108,7 +109,7 @@ class DoSignin(cmdbox_web_signin.Signin):
108
109
  last_update = web.user_data(None, uid, name, 'password', 'last_update')
109
110
  notify_passchange = True if last_update is None else False
110
111
  # パスワード認証の場合はパスワード有効期限チェック
111
- if user['hash']!='oauth2' and 'password' in signin_data and not notify_passchange:
112
+ if user['hash']!='oauth2' and user['hash']!='saml' and 'password' in signin_data and not notify_passchange:
112
113
  last_update = datetime.datetime.strptime(last_update, '%Y-%m-%dT%H:%M:%S')
113
114
  # パスワード有効期限
114
115
  expiration = signin_data['password']['expiration']
@@ -152,7 +153,7 @@ class DoSignin(cmdbox_web_signin.Signin):
152
153
  members = inspect.getmembers(mod, inspect.isclass)
153
154
  signin_data = web.signin.get_data()
154
155
  for name, cls in members:
155
- if cls is Signin or issubclass(cls, Signin):
156
+ if cls is signin.Signin or issubclass(cls, signin.Signin):
156
157
  sobj = cls(web.logger, web.signin_file, signin_data, appcls, ver)
157
158
  return sobj
158
159
  return None
@@ -161,9 +162,10 @@ class DoSignin(cmdbox_web_signin.Signin):
161
162
  raise e
162
163
 
163
164
  signin_data = web.signin.get_data()
164
- self.google_signin = Signin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
165
- self.github_signin = Signin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
166
- self.azure_signin = Signin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
165
+ self.google_signin = google_signin.GoogleSignin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
166
+ self.github_signin = github_signin.GithubSignin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
167
+ self.azure_signin = azure_signin.AzureSignin(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
168
+ self.azure_saml_signin = azure_signin_saml.AzyreSigninSAML(web.logger, web.signin_file, signin_data, self.appcls, self.ver)
167
169
  if signin_data is not None:
168
170
  # signinオブジェクトの指定があった場合読込む
169
171
  if 'signin_module' in signin_data['oauth2']['providers']['google']:
@@ -175,6 +177,9 @@ class DoSignin(cmdbox_web_signin.Signin):
175
177
  if 'signin_module' in signin_data['oauth2']['providers']['azure']:
176
178
  sobj = _load_signin(web, signin_data['oauth2']['providers']['azure']['signin_module'], self.appcls, self.ver)
177
179
  self.azure_signin = sobj if sobj is not None else self.azure_signin
180
+ if 'signin_module' in signin_data['saml']['providers']['azure']:
181
+ sobj = _load_signin(web, signin_data['saml']['providers']['azure']['signin_module'], self.appcls, self.ver)
182
+ self.azure_saml_signin = sobj if sobj is not None else self.azure_saml_signin
178
183
 
179
184
  def _set_session(req:Request, user:dict, email:str, hashed_password:str, access_token:str, group_names:list, gids:list):
180
185
  """
@@ -209,20 +214,10 @@ class DoSignin(cmdbox_web_signin.Signin):
209
214
  @app.get('/oauth2/google/callback')
210
215
  async def oauth2_google_callback(req:Request, res:Response):
211
216
  conf = web.signin.get_data()['oauth2']['providers']['google']
212
- headers = {'Content-Type': 'application/x-www-form-urlencoded'}
213
217
  next = req.query_params['state']
214
- data = {'code': req.query_params['code'],
215
- 'client_id': conf['client_id'],
216
- 'client_secret': conf['client_secret'],
217
- 'redirect_uri': conf['redirect_uri'],
218
- 'grant_type': 'authorization_code'}
219
- query = '&'.join([f'{k}={urllib.parse.quote(v)}' for k, v in data.items()])
220
218
  try:
221
219
  # アクセストークン取得
222
- token_resp = requests.post(url='https://oauth2.googleapis.com/token', headers=headers, data=query)
223
- token_resp.raise_for_status()
224
- token_json = token_resp.json()
225
- access_token = token_json['access_token']
220
+ access_token = self.google_signin.request_access_token(conf, req, res)
226
221
  return await oauth2_google_session(access_token, next, req, res)
227
222
  except Exception as e:
228
223
  web.logger.warning(f'Failed to get token. {e}', exc_info=True)
@@ -230,45 +225,15 @@ class DoSignin(cmdbox_web_signin.Signin):
230
225
 
231
226
  @app.get('/oauth2/google/session/{access_token}/{next}')
232
227
  async def oauth2_google_session(access_token:str, next:str, req:Request, res:Response):
233
- try:
234
- # ユーザー情報取得(email)
235
- user_info_resp = requests.get(
236
- url='https://www.googleapis.com/oauth2/v1/userinfo',
237
- headers={'Authorization': f'Bearer {access_token}'}
238
- )
239
- user_info_resp.raise_for_status()
240
- user_info_json = user_info_resp.json()
241
- email = user_info_json['email']
242
- # サインイン判定
243
- jadge, user = self.google_signin.jadge(access_token, email)
244
- if not jadge:
245
- return RedirectResponse(url=f'/signin/{next}?error=appdeny')
246
- # グループ取得
247
- group_names, gids = self.google_signin.get_groups(access_token, user)
248
- # セッションに保存
249
- _set_session(req, user, email, None, access_token, group_names, gids)
250
- return RedirectResponse(url=f'../../{next}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
251
- except Exception as e:
252
- web.logger.warning(f'Failed to get token. {e}', exc_info=True)
253
- raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
228
+ return await oauth2_login_session(self.google_signin, access_token, next, req, res)
254
229
 
255
230
  @app.get('/oauth2/github/callback')
256
231
  async def oauth2_github_callback(req:Request, res:Response):
257
232
  conf = web.signin.get_data()['oauth2']['providers']['github']
258
- headers = {'Content-Type': 'application/x-www-form-urlencoded',
259
- 'Accept': 'application/json'}
260
233
  next = req.query_params['state']
261
- data = {'code': req.query_params['code'],
262
- 'client_id': conf['client_id'],
263
- 'client_secret': conf['client_secret'],
264
- 'redirect_uri': conf['redirect_uri']}
265
- query = '&'.join([f'{k}={urllib.parse.quote(v)}' for k, v in data.items()])
266
234
  try:
267
235
  # アクセストークン取得
268
- token_resp = requests.post(url='https://github.com/login/oauth/access_token', headers=headers, data=query)
269
- token_resp.raise_for_status()
270
- token_json = token_resp.json()
271
- access_token = token_json['access_token']
236
+ access_token = self.github_signin.request_access_token(conf, req, res)
272
237
  return await oauth2_github_session(access_token, next, req, res)
273
238
  except Exception as e:
274
239
  web.logger.warning(f'Failed to get token. {e}', exc_info=True)
@@ -276,53 +241,15 @@ class DoSignin(cmdbox_web_signin.Signin):
276
241
 
277
242
  @app.get('/oauth2/github/session/{access_token}/{next}')
278
243
  async def oauth2_github_session(access_token:str, next:str, req:Request, res:Response):
279
- try:
280
- # ユーザー情報取得(email)
281
- user_info_resp = requests.get(
282
- url='https://api.github.com/user/emails',
283
- headers={'Authorization': f'Bearer {access_token}'}
284
- )
285
- user_info_resp.raise_for_status()
286
- user_info_json = user_info_resp.json()
287
- if type(user_info_json) == list:
288
- email = 'notfound'
289
- for u in user_info_json:
290
- if u['primary']:
291
- email = u['email']
292
- break
293
- # サインイン判定
294
- jadge, user = self.github_signin.jadge(access_token, email)
295
- if not jadge:
296
- return RedirectResponse(url=f'/signin/{next}?error=appdeny')
297
- # グループ取得
298
- group_names, gids = self.github_signin.get_groups(access_token, user)
299
- # セッションに保存
300
- _set_session(req, user, email, None, access_token, group_names, gids)
301
- return RedirectResponse(url=f'../../{next}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
302
- except Exception as e:
303
- web.logger.warning(f'Failed to get token. {e}', exc_info=True)
304
- raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
244
+ return await oauth2_login_session(self.github_signin, access_token, next, req, res)
305
245
 
306
246
  @app.get('/oauth2/azure/callback')
307
247
  async def oauth2_azure_callback(req:Request, res:Response):
308
248
  conf = web.signin.get_data()['oauth2']['providers']['azure']
309
- headers = {'Content-Type': 'application/x-www-form-urlencoded',
310
- 'Accept': 'application/json'}
311
249
  next = req.query_params['state']
312
- data = {'tenant': conf['tenant_id'],
313
- 'code': req.query_params['code'],
314
- 'scope': " ".join(conf['scope']),
315
- 'client_id': conf['client_id'],
316
- #'client_secret': conf['client_secret'],
317
- 'redirect_uri': conf['redirect_uri'],
318
- 'grant_type': 'authorization_code'}
319
- query = '&'.join([f'{k}={urllib.parse.quote(v)}' for k, v in data.items()])
320
250
  try:
321
251
  # アクセストークン取得
322
- token_resp = requests.post(url=f'https://login.microsoftonline.com/{conf["tenant_id"]}/oauth2/v2.0/token', headers=headers, data=query)
323
- token_resp.raise_for_status()
324
- token_json = token_resp.json()
325
- access_token = token_json['access_token']
252
+ access_token = self.azure_signin.request_access_token(conf, req, res)
326
253
  return await oauth2_azure_session(access_token, next, req, res)
327
254
  except Exception as e:
328
255
  web.logger.warning(f'Failed to get token. {e}', exc_info=True)
@@ -330,26 +257,75 @@ class DoSignin(cmdbox_web_signin.Signin):
330
257
 
331
258
  @app.get('/oauth2/azure/session/{access_token}/{next}')
332
259
  async def oauth2_azure_session(access_token:str, next:str, req:Request, res:Response):
260
+ return await oauth2_login_session(self.azure_signin, access_token, next, req, res)
261
+
262
+ async def oauth2_login_session(signin:signin.Signin, access_token:str, next:str, req:Request, res:Response):
333
263
  try:
334
264
  # ユーザー情報取得(email)
335
- user_info_resp = requests.get(
336
- url='https://graph.microsoft.com/v1.0/me',
337
- #url='https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$Top=999',
338
- headers={'Authorization': f'Bearer {access_token}'}
339
- )
340
- user_info_resp.raise_for_status()
341
- user_info_json = user_info_resp.json()
342
- if isinstance(user_info_json, dict):
343
- email = user_info_json.get('mail', 'notfound')
265
+ email = signin.get_email(access_token)
344
266
  # サインイン判定
345
- jadge, user = self.azure_signin.jadge(access_token, email)
267
+ jadge, user = signin.jadge(email)
346
268
  if not jadge:
347
269
  return RedirectResponse(url=f'/signin/{next}?error=appdeny')
348
270
  # グループ取得
349
- group_names, gids = self.azure_signin.get_groups(access_token, user)
271
+ group_names, gids = signin.get_groups(access_token, user)
350
272
  # セッションに保存
351
273
  _set_session(req, user, email, None, access_token, group_names, gids)
352
274
  return RedirectResponse(url=f'../../{next}', headers=dict(signin="success")) # nginxのリバプロ対応のための相対パス
353
275
  except Exception as e:
354
276
  web.logger.warning(f'Failed to get token. {e}', exc_info=True)
355
277
  raise HTTPException(status_code=500, detail=f'Failed to get token. {e}')
278
+
279
+ @app.post('/saml/azure/callback')
280
+ async def saml_azure_callback(req:Request, res:Response):
281
+ form = await req.form()
282
+ return await saml_login_callback('azure', self.azure_saml_signin, form, None, req, res)
283
+
284
+ @app.get('/saml/azure/session/{saml_token}/{next}')
285
+ async def saml_azure_session(saml_token:str, next:str, req:Request, res:Response):
286
+ form = json.loads(convert.b64str2str(saml_token))
287
+ return await saml_login_callback('azure', self.azure_saml_signin, form, next, req, res)
288
+
289
+ async def saml_login_callback(prov, saml_signin:signin_saml.SigninSAML, form:Dict[str, Any], next:str, req:Request, res:Response):
290
+ """
291
+ SAML認証のコールバック処理を行います
292
+ Args:
293
+ prov (str): SAMLプロバイダ名
294
+ saml_signin (signin_saml.SigninSAML): SAMLサインインオブジェクト
295
+ form (Dict[str, Any]): フォームデータ
296
+ req (Request): Requestオブジェクト
297
+ res (Response): Responseオブジェクト
298
+ """
299
+ relay = form.get('RelayState')
300
+ query = urllib.parse.urlparse(relay).query if relay is not None else None
301
+ if next is None:
302
+ next = urllib.parse.parse_qs(query).get('next', None) if query is not None else None
303
+ next = next[0] if next is not None and len(next) > 0 else None
304
+ auth = await saml_signin.make_saml(prov, next, form, req, res)
305
+ auth.process_response() # Process IdP response
306
+ errors = auth.get_errors() # This method receives an array with the errors
307
+ if len(errors) == 0:
308
+ if not auth.is_authenticated(): # This check if the response was ok and the user data retrieved or not (user authenticated)
309
+ return RedirectResponse(url=f'/signin/{next}?error=saml_not_auth')
310
+ else:
311
+ # ユーザー情報取得
312
+ email = saml_signin.get_email(auth)
313
+ # サインイン判定
314
+ jadge, user = saml_signin.jadge(email)
315
+ if not jadge:
316
+ return RedirectResponse(url=f'/signin/{next}?error=appdeny')
317
+ # グループ取得
318
+ group_names, gids = saml_signin.get_groups(None, user)
319
+ # セッションに保存
320
+ _set_session(req, user, email, None, None, group_names, gids)
321
+ # SAML場合、ブラウザ制限によりリダイレクトでセッションクッキーが消えるので、HTMLで移動する
322
+ html = """
323
+ <html><head><meta http-equiv="refresh" content="0;url=../../{next}"></head>
324
+ <body style="background-color:#212529;color:#fff;">loading..</body>
325
+ <script type="text/javascript">window.location.href="../../{next}";</script></html>
326
+ """.format(next=next)
327
+ return HTMLResponse(content=html, headers=dict(signin="success"))
328
+ else:
329
+ msg = f"Error when processing SAML Response: {', '.join(errors)} {auth.get_last_error_reason()}"
330
+ web.logger.warning(msg)
331
+ raise HTTPException(status_code=500, detail=msg)
@@ -1,9 +1,9 @@
1
- import urllib.parse
2
1
  from cmdbox.app import feature
3
2
  from cmdbox.app.web import Web
4
3
  from fastapi import FastAPI, Request, Response, HTTPException
5
4
  from fastapi.responses import HTMLResponse, RedirectResponse
6
5
  import urllib
6
+ import urllib.parse
7
7
 
8
8
 
9
9
  class Signin(feature.WebFeature):
@@ -83,3 +83,25 @@ class Signin(feature.WebFeature):
83
83
  return dict(google=signin_data['oauth2']['providers']['google']['enabled'],
84
84
  github=signin_data['oauth2']['providers']['github']['enabled'],
85
85
  azure=signin_data['oauth2']['providers']['azure']['enabled'],)
86
+
87
+ @app.get('/saml/{prov}/{next}')
88
+ async def saml_login(prov:str, next:str, req:Request, res:Response):
89
+ """
90
+ SAML認証のログイン処理を行います
91
+
92
+ Args:
93
+ prov (str): SAMLプロバイダ名
94
+ next (str): リダイレクト先のURL
95
+ req (Request): Requestオブジェクト
96
+ res (Response): Responseオブジェクト
97
+ """
98
+ form = await req.form()
99
+ auth = await web.signin_saml.make_saml(prov, next, form, req, res)
100
+ return RedirectResponse(url=auth.login())
101
+
102
+ @app.get('/saml/enabled')
103
+ async def saml_enabled(req:Request, res:Response):
104
+ if web.signin_html_data is None:
105
+ return dict(azure=False)
106
+ signin_data = web.signin_saml.get_data()
107
+ return dict(azure=signin_data['saml']['providers']['azure']['enabled'],)
cmdbox/app/web.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from cmdbox.app import common, options
2
- from cmdbox.app.auth.signin import Signin
2
+ from cmdbox.app.auth import signin, signin_saml
3
3
  from cmdbox.app.commons import module
4
4
  from fastapi import FastAPI, Request, Response, HTTPException
5
5
  from fastapi.responses import RedirectResponse
@@ -116,8 +116,9 @@ class Web:
116
116
  self.cb_queue = queue.Queue(1000)
117
117
  self.options = options.Options.getInstance()
118
118
  self.webcap_client = requests.Session()
119
- signin_file_data = Signin.load_signin_file(self.signin_file)
120
- self.signin = Signin(self.logger, self.signin_file, signin_file_data, self.appcls, self.ver)
119
+ signin_file_data = signin.Signin.load_signin_file(self.signin_file)
120
+ self.signin = signin.Signin(self.logger, self.signin_file, signin_file_data, self.appcls, self.ver)
121
+ self.signin_saml = signin_saml.SigninSAML(self.logger, self.signin_file, signin_file_data, self.appcls, self.ver)
121
122
 
122
123
  if self.logger.level == logging.DEBUG:
123
124
  self.logger.debug(f"web init parameter: data={self.data} -> {self.data.absolute() if self.data is not None else None}")
@@ -348,12 +349,12 @@ class Web:
348
349
  if 'hash' not in user or user['hash'] == '':
349
350
  raise ValueError(f"User hash is not found or empty. ({user})")
350
351
  hash = user['hash']
351
- if hash!='oauth2' and ('password' not in user or user['password'] == ''):
352
+ if hash!='oauth2' and hash!='saml' and ('password' not in user or user['password'] == ''):
352
353
  raise ValueError(f"User password is not found or empty. ({user})")
353
354
  if 'email' not in user:
354
355
  raise ValueError(f"User email is not found. ({user})")
355
- if hash=='oauth2' and (user['email'] is None or user['email']==''):
356
- raise ValueError(f"Required when `email` is `oauth2`. ({user})")
356
+ if (hash=='oauth2' or hash=='saml') and (user['email'] is None or user['email']==''):
357
+ raise ValueError(f"Required when `email` is `oauth2` or `saml`. ({user})")
357
358
  if 'groups' not in user or type(user['groups']) is not list:
358
359
  raise ValueError(f"User groups is not found or empty. ({user})")
359
360
  for gn in user['groups']:
@@ -363,13 +364,13 @@ class Web:
363
364
  raise ValueError(f"User uid is already exists. ({user})")
364
365
  if len([u for u in signin_data['users'] if u['name'] == user['name']]) > 0:
365
366
  raise ValueError(f"User name is already exists. ({user})")
366
- if hash not in ['oauth2', 'plain', 'md5', 'sha1', 'sha256']:
367
+ if hash not in ['oauth2', 'saml', 'plain', 'md5', 'sha1', 'sha256']:
367
368
  raise ValueError(f"User hash is not supported. ({user})")
368
369
  jadge, msg = self.signin.check_password_policy(user['name'], '', user['password'])
369
370
  if not jadge:
370
371
  raise ValueError(msg)
371
372
  if hash != 'plain':
372
- user['password'] = common.hash_password(user['password'], hash if hash != 'oauth2' else 'sha1')
373
+ user['password'] = common.hash_password(user['password'], hash if hash != 'oauth2' and hash != 'saml' else 'sha1')
373
374
  else:
374
375
  user['password'] = user['password']
375
376
  signin_data['users'].append(user)
@@ -405,8 +406,8 @@ class Web:
405
406
  if 'email' not in user:
406
407
  raise ValueError(f"User email is not found. ({user})")
407
408
  hash = user['hash']
408
- if hash=='oauth2' and (user['email'] is None or user['email']==''):
409
- raise ValueError(f"Required when `email` is `oauth2`. ({user})")
409
+ if (hash=='oauth2' or hash=='saml') and (user['email'] is None or user['email']==''):
410
+ raise ValueError(f"Required when `email` is `oauth2` or `saml`. ({user})")
410
411
  if 'groups' not in user or type(user['groups']) is not list:
411
412
  raise ValueError(f"User groups is not found or empty. ({user})")
412
413
  for gn in user['groups']:
@@ -416,7 +417,7 @@ class Web:
416
417
  raise ValueError(f"User uid is not found. ({user})")
417
418
  if len([u for u in signin_data['users'] if u['name'] == user['name']]) <= 0:
418
419
  raise ValueError(f"User name is not found. ({user})")
419
- if hash not in ['oauth2', 'plain', 'md5', 'sha1', 'sha256']:
420
+ if hash not in ['oauth2', 'saml', 'plain', 'md5', 'sha1', 'sha256']:
420
421
  raise ValueError(f"User hash is not supported. ({user})")
421
422
  for u in signin_data['users']:
422
423
  if u['uid'] == user['uid']:
@@ -426,7 +427,7 @@ class Web:
426
427
  if not jadge:
427
428
  raise ValueError(msg)
428
429
  if hash != 'plain':
429
- u['password'] = common.hash_password(user['password'], hash if hash != 'oauth2' else 'sha1')
430
+ u['password'] = common.hash_password(user['password'], hash if hash != 'oauth2' and hash != 'saml' else 'sha1')
430
431
  else:
431
432
  u['password'] = user['password']
432
433
  # パスワード更新日時の保存
@@ -46,3 +46,26 @@ aliases: # Specify the alias for the specified co
46
46
  # e.g. /{1}_exec
47
47
  move: # Specify whether to move the regular expression group of the source to the target.
48
48
  # e.g. true
49
+ audit:
50
+ enabled: true # Specify whether to enable the audit function.
51
+ write:
52
+ mode: audit # Specify the mode of the feature to be writed.
53
+ cmd: write # Specify the command to be writed.
54
+ search:
55
+ mode: audit # Specify the mode of the feature to be searched.
56
+ cmd: search # Specify the command to be searched.
57
+ options: # Specify the options for the audit function.
58
+ host: localhost # Specify the service host of the audit Redis server.However, if it is specified as a command line argument, it is ignored.
59
+ port: 6379 # Specify the service port of the audit Redis server.However, if it is specified as a command line argument, it is ignored.
60
+ password: password # Specify the access password of the audit Redis server.However, if it is specified as a command line argument, it is ignored.
61
+ svname: cmdbox # Specify the audit service name of the inference server.However, if it is specified as a command line argument, it is ignored.
62
+ retry_count: 3 # Specifies the number of reconnections to the audit Redis server.If less than 0 is specified, reconnection is forever.
63
+ retry_interval: 1 # Specifies the number of seconds before reconnecting to the audit Redis server.
64
+ timeout: 15 # Specify the maximum waiting time until the server responds.
65
+ pg_enabled: False # Specify True if using the postgresql database server.
66
+ pg_host: localhost # Specify the postgresql host.
67
+ pg_port: 5432 # Specify the postgresql port.
68
+ pg_user: postgres # Specify the postgresql user name.
69
+ pg_password: password # Specify the postgresql password.
70
+ pg_dbname: audit # Specify the postgresql database name.
71
+ retention_period_days: 365 # Specify the number of days to retain audit logs.
@@ -2,9 +2,9 @@ users: # A list of users, each of which is a map that co
2
2
  - uid: 1 # An ID that identifies a user. No two users can have the same ID.
3
3
  name: admin # A name that identifies the user. No two users can have the same name.
4
4
  password: admin # The user's password. The value is hashed with the hash function specified in the next hash field.
5
- hash: plain # The hash function used to hash the password, which can be plain, md5, sha1, or sha256, or oauth2.
5
+ hash: plain # The hash function used to hash the password, which can be plain, md5, sha1, or sha256, or oauth2, or saml.
6
6
  groups: [admin] # A list of groups to which the user belongs, as specified in the groups field.
7
- email: admin@aaa.bbb.jp # The email address of the user, used when authenticating using the provider specified in the oauth2 field.
7
+ email: admin@aaa.bbb.jp # The email address of the user, used when authenticating using the provider specified in the oauth2 or saml field.
8
8
  - uid: 101
9
9
  name: user01
10
10
  password: b75705d7e35e7014521a46b532236ec3
@@ -36,7 +36,6 @@ groups: # A list of groups, each of which is a map that c
36
36
  - gid: 103
37
37
  name: editor
38
38
  parent: user
39
-
40
39
  cmdrule: # A list of command rules, Specify a rule that determines whether or not a command is executable when executed by a user in web mode.
41
40
  policy: deny # Specify the default policy for the rule. The value can be allow or deny.
42
41
  rules: # Specify rules to allow or deny execution of the command, depending on the group the user belongs to.
@@ -50,6 +49,10 @@ cmdrule: # A list of command rules, Specify a rule that de
50
49
  mode: server
51
50
  cmds: [list]
52
51
  rule: allow
52
+ - groups: [user, guest]
53
+ mode: audit
54
+ cmds: [write]
55
+ rule: allow
53
56
  - groups: [user, guest]
54
57
  mode: web
55
58
  cmds: [genpass]
@@ -70,6 +73,7 @@ pathrule: # List of RESTAPI rules, rules that determine whe
70
73
  rule: allow
71
74
  - groups: [user]
72
75
  paths: [/signin, /assets, /bbforce_cmd, /copyright, /dosignin, /dosignout, /password/change,
76
+ /gui/user_data/load, /gui/user_data/save, /gui/user_data/delete,
73
77
  /exec_cmd, /exec_pipe, /filer, /gui, /get_server_opt, /usesignout, /versions_cmdbox, /versions_used]
74
78
  rule: allow
75
79
  - groups: [readonly]
@@ -105,7 +109,8 @@ oauth2: # OAuth2 settings.
105
109
  client_secret: XXXXXXXXXXX # Specify Google's OAuth2 client secret.
106
110
  redirect_uri: https://localhost:8443/oauth2/google/callback # Specify Google's OAuth2 redirect URI.
107
111
  scope: ['email'] # Specify the scope you want to retrieve with Google's OAuth2. Usually, just reading the email is sufficient.
108
- signin_module: # Specify the module name that implements the sign-in. see, cmdbox.app.signin.SignIn
112
+ signin_module: # Specify the module name that implements the sign-in.
113
+ cmdbox.app.auth.google_signin
109
114
  note: # Specify a description such as Google's OAuth2 reference site.
110
115
  - https://developers.google.com/identity/protocols/oauth2/web-server?hl=ja#httprest
111
116
  github: # OAuth2 settings for GitHub.
@@ -114,7 +119,8 @@ oauth2: # OAuth2 settings.
114
119
  client_secret: XXXXXXXXXXX # Specify the GitHub OAuth2 client secret.
115
120
  redirect_uri: https://localhost:8443/oauth2/github/callback # Specify the OAuth2 redirect URI for GitHub.
116
121
  scope: ['user:email'] # Specify the scope you want to get from GitHub's OAuth2. Usually, just reading the email is sufficient.
117
- signin_module: # Specify the module name that implements the sign-in. see, cmdbox.app.signin.SignIn
122
+ signin_module: # Specify the module name that implements the sign-in.
123
+ cmdbox.app.auth.github_signin
118
124
  note: # Specify a description, such as a reference site for OAuth2 on GitHub.
119
125
  - https://docs.github.com/ja/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#scopes
120
126
  azure: # OAuth2 settings for Azure AD.
@@ -124,6 +130,34 @@ oauth2: # OAuth2 settings.
124
130
  client_secret: XXXXXXXXXXX # Specify the Azure AD OAuth2 client secret.
125
131
  redirect_uri: https://localhost:8443/oauth2/azure/callback # Specify the OAuth2 redirect URI for Azure AD.
126
132
  scope: ['openid', 'profile', 'email', 'https://graph.microsoft.com/mail.read']
127
- signin_module: # Specify the module name that implements the sign-in. see, cmdbox.app.signin.SignIn
133
+ signin_module: # Specify the module name that implements the sign-in.
134
+ cmdbox.app.auth.azure_signin
128
135
  note: # Specify a description, such as a reference site for Azure AD's OAuth2.
129
136
  - https://learn.microsoft.com/ja-jp/entra/identity-platform/v2-oauth2-auth-code-flow
137
+ saml: # SAML settings.
138
+ providers: # This is a per-provider setting for OAuth2.
139
+ azure: # SAML settings for Azure AD.
140
+ enabled: false # Specify whether to enable SAML authentication for Azure AD.
141
+ signin_module: # Specify the module name that implements the sign-in.
142
+ cmdbox.app.auth.azure_signin_saml # Specify the python3-saml configuration.
143
+ # see) https://github.com/SAML-Toolkits/python3-saml
144
+ sp:
145
+ entityId: https://localhost:8443/
146
+ assertionConsumerService:
147
+ url: https://localhost:8443/saml/azure/callback
148
+ binding: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
149
+ attributeConsumingService: {}
150
+ singleLogoutService:
151
+ binding: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
152
+ NameIDFormat: urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
153
+ x509cert: ''
154
+ privateKey: ''
155
+ idp:
156
+ entityId: https://sts.windows.net/{tenant-id}/
157
+ singleSignOnService:
158
+ url: https://login.microsoftonline.com/{tenant-id}/saml2
159
+ binding: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
160
+ x509cert: XXXXXXXXXXX
161
+ singleLogoutService: {}
162
+ certFingerprint: ''
163
+ certFingerprintAlgorithm: sha1
@@ -2,9 +2,9 @@ users: # A list of users, each of which is a map that co
2
2
  - uid: 1 # An ID that identifies a user. No two users can have the same ID.
3
3
  name: admin # A name that identifies the user. No two users can have the same name.
4
4
  password: admin # The user's password. The value is hashed with the hash function specified in the next hash field.
5
- hash: plain # The hash function used to hash the password, which can be plain, md5, sha1, or sha256, or oauth2.
5
+ hash: plain # The hash function used to hash the password, which can be plain, md5, sha1, or sha256, or oauth2, or saml.
6
6
  groups: [admin] # A list of groups to which the user belongs, as specified in the groups field.
7
- email: admin@aaa.bbb.jp # The email address of the user, used when authenticating using the provider specified in the oauth2 field.
7
+ email: admin@aaa.bbb.jp # The email address of the user, used when authenticating using the provider specified in the oauth2 or saml field.
8
8
  - uid: 101
9
9
  name: user01
10
10
  password: b75705d7e35e7014521a46b532236ec3
@@ -74,7 +74,7 @@ pathrule: # List of RESTAPI rules, rules that determine whe
74
74
  - groups: [user]
75
75
  paths: [/signin, /assets, /bbforce_cmd, /copyright, /dosignin, /dosignout, /password/change,
76
76
  /gui/user_data/load, /gui/user_data/save, /gui/user_data/delete,
77
- /exec_cmd, /exec_pipe, /filer, /gui, /get_server_opt, /usesignout, /versions_cmdbox, /versions_used]
77
+ /exec_cmd, /exec_pipe, /filer, /result, /gui, /get_server_opt, /usesignout, /versions_cmdbox, /versions_used]
78
78
  rule: allow
79
79
  - groups: [readonly]
80
80
  paths: [/gui/del_cmd, /gui/del_pipe, /gui/save_cmd, /gui/save_pipe]
@@ -109,7 +109,8 @@ oauth2: # OAuth2 settings.
109
109
  client_secret: XXXXXXXXXXX # Specify Google's OAuth2 client secret.
110
110
  redirect_uri: https://localhost:8443/oauth2/google/callback # Specify Google's OAuth2 redirect URI.
111
111
  scope: ['email'] # Specify the scope you want to retrieve with Google's OAuth2. Usually, just reading the email is sufficient.
112
- signin_module: # Specify the module name that implements the sign-in. see, cmdbox.app.signin.SignIn
112
+ signin_module: # Specify the module name that implements the sign-in.
113
+ cmdbox.app.auth.google_signin
113
114
  note: # Specify a description such as Google's OAuth2 reference site.
114
115
  - https://developers.google.com/identity/protocols/oauth2/web-server?hl=ja#httprest
115
116
  github: # OAuth2 settings for GitHub.
@@ -118,7 +119,8 @@ oauth2: # OAuth2 settings.
118
119
  client_secret: XXXXXXXXXXX # Specify the GitHub OAuth2 client secret.
119
120
  redirect_uri: https://localhost:8443/oauth2/github/callback # Specify the OAuth2 redirect URI for GitHub.
120
121
  scope: ['user:email'] # Specify the scope you want to get from GitHub's OAuth2. Usually, just reading the email is sufficient.
121
- signin_module: # Specify the module name that implements the sign-in. see, cmdbox.app.signin.SignIn
122
+ signin_module: # Specify the module name that implements the sign-in.
123
+ cmdbox.app.auth.github_signin
122
124
  note: # Specify a description, such as a reference site for OAuth2 on GitHub.
123
125
  - https://docs.github.com/ja/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#scopes
124
126
  azure: # OAuth2 settings for Azure AD.
@@ -128,6 +130,34 @@ oauth2: # OAuth2 settings.
128
130
  client_secret: XXXXXXXXXXX # Specify the Azure AD OAuth2 client secret.
129
131
  redirect_uri: https://localhost:8443/oauth2/azure/callback # Specify the OAuth2 redirect URI for Azure AD.
130
132
  scope: ['openid', 'profile', 'email', 'https://graph.microsoft.com/mail.read']
131
- signin_module: # Specify the module name that implements the sign-in. see, cmdbox.app.signin.SignIn
133
+ signin_module: # Specify the module name that implements the sign-in.
134
+ cmdbox.app.auth.azure_signin
132
135
  note: # Specify a description, such as a reference site for Azure AD's OAuth2.
133
136
  - https://learn.microsoft.com/ja-jp/entra/identity-platform/v2-oauth2-auth-code-flow
137
+ saml: # SAML settings.
138
+ providers: # This is a per-provider setting for OAuth2.
139
+ azure: # SAML settings for Azure AD.
140
+ enabled: false # Specify whether to enable SAML authentication for Azure AD.
141
+ signin_module: # Specify the module name that implements the sign-in.
142
+ cmdbox.app.auth.azure_signin_saml # Specify the python3-saml configuration.
143
+ # see) https://github.com/SAML-Toolkits/python3-saml
144
+ sp:
145
+ entityId: https://localhost:8443/
146
+ assertionConsumerService:
147
+ url: https://localhost:8443/saml/azure/callback
148
+ binding: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
149
+ attributeConsumingService: {}
150
+ singleLogoutService:
151
+ binding: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
152
+ NameIDFormat: urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
153
+ x509cert: ''
154
+ privateKey: ''
155
+ idp:
156
+ entityId: https://sts.windows.net/{tenant-id}/
157
+ singleSignOnService:
158
+ url: https://login.microsoftonline.com/{tenant-id}/saml2
159
+ binding: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
160
+ x509cert: XXXXXXXXXXX
161
+ singleLogoutService: {}
162
+ certFingerprint: ''
163
+ certFingerprintAlgorithm: sha1
@@ -1,14 +1,13 @@
1
- Copyright 2012, Andrew Dunham
1
+ Copyright 2016-2020 aio-libs collaboration.
2
2
 
3
3
  Licensed under the Apache License, Version 2.0 (the "License");
4
4
  you may not use this file except in compliance with the License.
5
5
  You may obtain a copy of the License at
6
6
 
7
- https://www.apache.org/licenses/LICENSE-2.0
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
8
 
9
9
  Unless required by applicable law or agreed to in writing, software
10
10
  distributed under the License is distributed on an "AS IS" BASIS,
11
11
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
-