cmdbox 0.5.3__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.
- cmdbox/app/auth/__init__.py +0 -0
- cmdbox/app/auth/azure_signin.py +38 -0
- cmdbox/app/auth/azure_signin_saml.py +12 -0
- cmdbox/app/auth/github_signin.py +38 -0
- cmdbox/app/auth/google_signin.py +32 -0
- cmdbox/app/auth/signin.py +47 -4
- cmdbox/app/auth/signin_saml.py +61 -0
- cmdbox/app/edge.py +198 -61
- cmdbox/app/feature.py +2 -1
- cmdbox/app/features/cli/audit_base.py +1 -1
- cmdbox/app/features/cli/cmdbox_audit_createdb.py +1 -1
- cmdbox/app/features/cli/cmdbox_audit_write.py +4 -0
- cmdbox/app/features/cli/cmdbox_client_file_copy.py +1 -1
- cmdbox/app/features/cli/cmdbox_client_file_download.py +1 -1
- cmdbox/app/features/cli/cmdbox_client_file_list.py +1 -1
- cmdbox/app/features/cli/cmdbox_client_file_mkdir.py +1 -1
- cmdbox/app/features/cli/cmdbox_client_file_move.py +1 -1
- cmdbox/app/features/cli/cmdbox_client_file_remove.py +1 -1
- cmdbox/app/features/cli/cmdbox_client_file_rmdir.py +1 -1
- cmdbox/app/features/cli/cmdbox_client_file_upload.py +1 -1
- cmdbox/app/features/cli/cmdbox_client_server_info.py +1 -1
- cmdbox/app/features/cli/cmdbox_edge_config.py +19 -5
- cmdbox/app/features/cli/cmdbox_gui_start.py +1 -1
- cmdbox/app/features/cli/cmdbox_server_start.py +1 -1
- cmdbox/app/features/cli/cmdbox_server_stop.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_apikey_add.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_apikey_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_add.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_edit.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_list.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_start.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_add.py +4 -4
- cmdbox/app/features/cli/cmdbox_web_user_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_edit.py +4 -4
- cmdbox/app/features/cli/cmdbox_web_user_list.py +1 -1
- cmdbox/app/features/web/cmdbox_web_audit.py +7 -1
- cmdbox/app/features/web/cmdbox_web_do_signin.py +79 -103
- cmdbox/app/features/web/cmdbox_web_exec_cmd.py +2 -2
- cmdbox/app/features/web/cmdbox_web_signin.py +23 -1
- cmdbox/app/options.py +9 -0
- cmdbox/app/server.py +15 -3
- cmdbox/app/web.py +13 -12
- cmdbox/extensions/features.yml +4 -4
- cmdbox/extensions/sample_project/sample/app/features/cli/sample_server_time.py +1 -1
- cmdbox/extensions/sample_project/sample/extensions/features.yml +23 -0
- cmdbox/extensions/sample_project/sample/extensions/user_list.yml +40 -6
- cmdbox/extensions/user_list.yml +36 -6
- cmdbox/licenses/LICENSE.async-timeout.5.0.1(Apache Software License).txt +13 -0
- cmdbox/licenses/files.txt +10 -9
- cmdbox/version.py +2 -2
- cmdbox/web/assets/cmdbox/audit.js +98 -34
- cmdbox/web/assets/cmdbox/signin.js +13 -0
- cmdbox/web/assets/cmdbox/users.js +1 -1
- cmdbox/web/audit.html +69 -44
- cmdbox/web/signin.html +10 -6
- {cmdbox-0.5.3.dist-info → cmdbox-0.5.4.dist-info}/METADATA +69 -15
- {cmdbox-0.5.3.dist-info → cmdbox-0.5.4.dist-info}/RECORD +71 -79
- cmdbox/app/features/web/cmdbox_web_load_pin.py +0 -43
- cmdbox/app/features/web/cmdbox_web_save_pin.py +0 -42
- cmdbox/licenses/LICENSE.argcomplete.3.6.1(Apache Software License).txt +0 -177
- cmdbox/licenses/LICENSE.gevent.25.4.1(MIT).txt +0 -25
- cmdbox/licenses/LICENSE.greenlet.3.2.0(MIT AND Python-2.0).txt +0 -30
- cmdbox/licenses/LICENSE.pillow.11.1.0(CMU License (MIT-CMU)).txt +0 -1213
- cmdbox/licenses/LICENSE.prompt_toolkit.3.0.50(BSD License).txt +0 -27
- cmdbox/licenses/LICENSE.psycopg-pool.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +0 -165
- cmdbox/licenses/LICENSE.pydantic.2.11.1(MIT License).txt +0 -21
- cmdbox/licenses/LICENSE.pydantic_core.2.33.0(MIT License).txt +0 -21
- cmdbox/licenses/LICENSE.starlette.0.46.1(BSD License).txt +0 -27
- cmdbox/licenses/LICENSE.typing_extensions.4.13.0(UNKNOWN).txt +0 -279
- cmdbox/licenses/LICENSE.urllib3.2.3.0(MIT License).txt +0 -21
- cmdbox/licenses/LICENSE.uvicorn.0.34.1(BSD License).txt +0 -27
- cmdbox/licenses/LICENSE.watchfiles.1.0.4(MIT License).txt +0 -21
- /cmdbox/licenses/{LICENSE.certifi.2025.1.31(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
- /cmdbox/licenses/{LICENSE.gevent.24.11.1(MIT License).txt → LICENSE.gevent.25.4.2(MIT).txt} +0 -0
- /cmdbox/licenses/{LICENSE.greenlet.3.1.1(MIT License).txt → LICENSE.greenlet.3.2.1(MIT AND Python-2.0).txt} +0 -0
- /cmdbox/licenses/{LICENSE.h11.0.14.0(MIT License).txt → LICENSE.h11.0.16.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.importlib_metadata.8.6.1(Apache Software License).txt → LICENSE.importlib_metadata.8.7.0(Apache Software License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.more-itertools.10.6.0(MIT License).txt → LICENSE.more-itertools.10.7.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.numpy.2.2.4(BSD License).txt → LICENSE.numpy.2.2.5(BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.packaging.24.2(Apache Software License; BSD License).txt → LICENSE.packaging.25.0(Apache Software License; BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.uvicorn.0.34.0(BSD License).txt → LICENSE.uvicorn.0.34.2(BSD License).txt} +0 -0
- {cmdbox-0.5.3.dist-info → cmdbox-0.5.4.dist-info}/LICENSE +0 -0
- {cmdbox-0.5.3.dist-info → cmdbox-0.5.4.dist-info}/WHEEL +0 -0
- {cmdbox-0.5.3.dist-info → cmdbox-0.5.4.dist-info}/entry_points.txt +0 -0
- {cmdbox-0.5.3.dist-info → cmdbox-0.5.4.dist-info}/top_level.txt +0 -0
|
@@ -47,7 +47,7 @@ class GuiStart(feature.UnsupportEdgeFeature):
|
|
|
47
47
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
48
48
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
49
49
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
50
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
50
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
51
51
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
52
52
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
53
53
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -46,7 +46,7 @@ class ServerStart(feature.OneshotNotifyEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -47,7 +47,7 @@ class ServerStop(feature.OneshotNotifyEdgeFeature):
|
|
|
47
47
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
48
48
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
49
49
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
50
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
50
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
51
51
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
52
52
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
53
53
|
dict(opt="retry_count", type=Options.T_INT, default=3, required=False, multi=False, hide=True, choice=None,
|
|
@@ -46,7 +46,7 @@ class WebApikeyAdd(feature.UnsupportEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -46,7 +46,7 @@ class WebApikeyDel(feature.UnsupportEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -46,7 +46,7 @@ class WebGroupAdd(feature.UnsupportEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -46,7 +46,7 @@ class WebGroupDel(feature.UnsupportEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -46,7 +46,7 @@ class WebGroupEdit(feature.UnsupportEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -46,7 +46,7 @@ class WebGroupList(feature.OneshotResultEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -47,7 +47,7 @@ class WebStart(feature.UnsupportEdgeFeature):
|
|
|
47
47
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
48
48
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
49
49
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
50
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
50
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
51
51
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
52
52
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
53
53
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -46,7 +46,7 @@ class WebUserAdd(feature.UnsupportEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -61,12 +61,12 @@ class WebUserAdd(feature.UnsupportEdgeFeature):
|
|
|
61
61
|
dict(opt="user_pass", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
|
|
62
62
|
discription_ja="ユーザーパスワードを指定します。",
|
|
63
63
|
discription_en="Specify the user password."),
|
|
64
|
-
dict(opt="user_pass_hash", type=Options.T_STR, default='sha1', required=False, multi=False, hide=False, choice=['oauth2', 'plain', 'md5', 'sha1', 'sha256'],
|
|
64
|
+
dict(opt="user_pass_hash", type=Options.T_STR, default='sha1', required=False, multi=False, hide=False, choice=['oauth2', 'saml', 'plain', 'md5', 'sha1', 'sha256'],
|
|
65
65
|
discription_ja="ユーザーパスワードのハッシュアルゴリズムを指定します。",
|
|
66
66
|
discription_en="Specifies the hash algorithm for user passwords."),
|
|
67
67
|
dict(opt="user_email", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
|
|
68
|
-
discription_ja="ユーザーメールアドレスを指定します。 `user_pass_hash` が `oauth2` の時は必須です。",
|
|
69
|
-
discription_en="Specify the user email. Required when `user_pass_hash` is `oauth2`."),
|
|
68
|
+
discription_ja="ユーザーメールアドレスを指定します。 `user_pass_hash` が `oauth2` 又は `saml` の時は必須です。",
|
|
69
|
+
discription_en="Specify the user email. Required when `user_pass_hash` is `oauth2` or `saml`."),
|
|
70
70
|
dict(opt="user_group", type=Options.T_STR, default=None, required=True, multi=True, hide=False, choice=None,
|
|
71
71
|
discription_ja="ユーザーが所属するグループを指定します。",
|
|
72
72
|
discription_en="Specifies the groups to which the user belongs."),
|
|
@@ -46,7 +46,7 @@ class WebUserDel(feature.UnsupportEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -46,7 +46,7 @@ class WebUserEdit(feature.UnsupportEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -61,12 +61,12 @@ class WebUserEdit(feature.UnsupportEdgeFeature):
|
|
|
61
61
|
dict(opt="user_pass", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
|
|
62
62
|
discription_ja="ユーザーパスワードを指定します。",
|
|
63
63
|
discription_en="Specify the user password."),
|
|
64
|
-
dict(opt="user_pass_hash", type=Options.T_STR, default='sha1', required=False, multi=False, hide=False, choice=['oauth2', 'plain', 'md5', 'sha1', 'sha256'],
|
|
64
|
+
dict(opt="user_pass_hash", type=Options.T_STR, default='sha1', required=False, multi=False, hide=False, choice=['oauth2', 'saml', 'plain', 'md5', 'sha1', 'sha256'],
|
|
65
65
|
discription_ja="ユーザーパスワードのハッシュアルゴリズムを指定します。",
|
|
66
66
|
discription_en="Specifies the hash algorithm for user passwords."),
|
|
67
67
|
dict(opt="user_email", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
|
|
68
|
-
discription_ja="ユーザーメールアドレスを指定します。 `user_pass_hash` が `oauth2` の時は必須です。",
|
|
69
|
-
discription_en="Specify the user email. Required when `user_pass_hash` is `oauth2`."),
|
|
68
|
+
discription_ja="ユーザーメールアドレスを指定します。 `user_pass_hash` が `oauth2` 又は `saml` の時は必須です。",
|
|
69
|
+
discription_en="Specify the user email. Required when `user_pass_hash` is `oauth2` or `saml`."),
|
|
70
70
|
dict(opt="user_group", type=Options.T_STR, default=None, required=True, multi=True, hide=False, choice=None,
|
|
71
71
|
discription_ja="ユーザーが所属するグループを指定します。",
|
|
72
72
|
discription_en="Specifies the groups to which the user belongs."),
|
|
@@ -46,7 +46,7 @@ class WebUserList(feature.OneshotResultEdgeFeature):
|
|
|
46
46
|
dict(opt="password", type=Options.T_STR, default=self.default_pass, required=True, multi=False, hide=True, choice=None, web="mask",
|
|
47
47
|
discription_ja="Redisサーバーのアクセスパスワード(任意)を指定します。省略時は `password` を使用します。",
|
|
48
48
|
discription_en="Specify the access password of the Redis server (optional). If omitted, `password` is used."),
|
|
49
|
-
dict(opt="svname", type=Options.T_STR, default=
|
|
49
|
+
dict(opt="svname", type=Options.T_STR, default=self.default_svname, required=True, multi=False, hide=True, choice=None, web="readonly",
|
|
50
50
|
discription_ja="サーバーのサービス名を指定します。省略時は `server` を使用します。",
|
|
51
51
|
discription_en="Specify the service name of the inference server. If omitted, `server` is used."),
|
|
52
52
|
dict(opt="data", type=Options.T_FILE, default=common.HOME_DIR / f".{self.ver.__appid__}", required=False, multi=False, hide=False, choice=None,
|
|
@@ -41,9 +41,13 @@ class Audit(feature.WebFeature):
|
|
|
41
41
|
if web.signin.get_data() is None:
|
|
42
42
|
return dict(error='signin_file_data is None.')
|
|
43
43
|
if not hasattr(web.options, 'audit_search') or web.options.audit_search is None:
|
|
44
|
-
|
|
44
|
+
return dict(warn='audit feature is disabled.')
|
|
45
45
|
opt = await req.json()
|
|
46
46
|
opt = {**opt, **web.options.audit_search_args.copy()}
|
|
47
|
+
opt['host'] = web.redis_host
|
|
48
|
+
opt['port'] = web.redis_port
|
|
49
|
+
opt['password'] = web.redis_password
|
|
50
|
+
opt['svname'] = web.svname
|
|
47
51
|
args = argparse.Namespace(**{k:common.chopdq(v) for k,v in opt.items()})
|
|
48
52
|
status, ret_main, _ = web.options.audit_search.apprun(web.logger, args, time.perf_counter(), [])
|
|
49
53
|
if status != 0:
|
|
@@ -55,6 +59,8 @@ class Audit(feature.WebFeature):
|
|
|
55
59
|
signin = web.signin.check_signin(req, res)
|
|
56
60
|
if signin is not None:
|
|
57
61
|
return signin
|
|
62
|
+
if not hasattr(web.options, 'audit_search_args'):
|
|
63
|
+
return dict(warn='audit feature is disabled.')
|
|
58
64
|
return dict(success=web.options.audit_search_args)
|
|
59
65
|
|
|
60
66
|
def toolmenu(self, web:Web) -> Dict[str, Any]:
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
import urllib.parse
|
|
1
2
|
from cmdbox.app import common
|
|
2
|
-
from cmdbox.app.auth
|
|
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
|
|
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 =
|
|
165
|
-
self.github_signin =
|
|
166
|
-
self.azure_signin =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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)
|
|
@@ -136,9 +136,9 @@ class ExecCmd(cmdbox_web_load_cmd.LoadCmd):
|
|
|
136
136
|
found = True
|
|
137
137
|
if not found or o not in loaded: continue
|
|
138
138
|
opt[o] = loaded[o]
|
|
139
|
-
if isinstance(feat, cmdbox_audit_write.AuditWrite) and o in _options.audit_write_args:
|
|
139
|
+
if isinstance(feat, cmdbox_audit_write.AuditWrite) and hasattr(_options, 'audit_write_args') and o in _options.audit_write_args:
|
|
140
140
|
opt[o] = _options.audit_write_args[o]
|
|
141
|
-
elif isinstance(feat, cmdbox_audit_search.AuditSearch) and o in _options.audit_search_args:
|
|
141
|
+
elif isinstance(feat, cmdbox_audit_search.AuditSearch) and hasattr(_options, 'audit_search_args') and o in _options.audit_search_args:
|
|
142
142
|
opt[o] = _options.audit_search_args[o]
|
|
143
143
|
except:
|
|
144
144
|
pass
|
|
@@ -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/options.py
CHANGED
|
@@ -713,6 +713,11 @@ class Options:
|
|
|
713
713
|
user (str): メッセージを発生させたユーザー名
|
|
714
714
|
kwargs (Any): 呼び出し元で使用しているキーワード引数
|
|
715
715
|
"""
|
|
716
|
+
if not hasattr(self, 'audit_write_args') or self.audit_write_args is None:
|
|
717
|
+
return
|
|
718
|
+
yml = self.features_yml_data
|
|
719
|
+
if yml is None or 'audit' not in yml or 'enabled' not in yml['audit'] or not yml['audit']['enabled']:
|
|
720
|
+
return
|
|
716
721
|
if not hasattr(self, 'audit_write') or self.audit_write is None:
|
|
717
722
|
raise Exception('audit write feature is not found.')
|
|
718
723
|
clmsg_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + common.get_tzoffset_str()
|
|
@@ -761,6 +766,10 @@ class Options:
|
|
|
761
766
|
else:
|
|
762
767
|
clmsg_body[key] = common.to_str(val, 100)
|
|
763
768
|
opt[key] = val
|
|
769
|
+
if hasattr(arg, 'redis_host'): opt['host'] = arg.redis_host
|
|
770
|
+
if hasattr(arg, 'redis_port'): opt['port'] = arg.redis_port
|
|
771
|
+
if hasattr(arg, 'redis_password'): opt['password'] = arg.redis_password
|
|
772
|
+
if hasattr(arg, 'svname'): opt['svname'] = arg.svname
|
|
764
773
|
if hasattr(arg, 'clmsg_id'): opt['clmsg_id'] = arg.clmsg_id
|
|
765
774
|
elif isinstance(arg, web.Web):
|
|
766
775
|
opt['host'] = arg.redis_host
|
cmdbox/app/server.py
CHANGED
|
@@ -133,6 +133,7 @@ class Server(filer.Filer):
|
|
|
133
133
|
|
|
134
134
|
while self.is_running:
|
|
135
135
|
try:
|
|
136
|
+
msg = None
|
|
136
137
|
# ブロッキングリストから要素を取り出す
|
|
137
138
|
ctime = time.time()
|
|
138
139
|
self.redis_cli.hset(self.redis_cli.hbname, 'ctime', ctime)
|
|
@@ -186,18 +187,29 @@ class Server(filer.Filer):
|
|
|
186
187
|
except exceptions.TimeoutError:
|
|
187
188
|
pass
|
|
188
189
|
except exceptions.ConnectionError as e:
|
|
189
|
-
self.logger.warning(f"Connection to the server was lost. {e}",
|
|
190
|
+
self.logger.warning(f"Connection to the server was lost. {e}", exc_info=True)
|
|
190
191
|
if not self.redis_cli.check_server(find_svname=False, retry_count=self.retry_count, retry_interval=self.retry_interval, outstatus=True):
|
|
191
192
|
self.is_running = False
|
|
192
193
|
break
|
|
193
194
|
except OSError as e:
|
|
194
|
-
self.logger.warning(f"OSError. {e}",
|
|
195
|
+
self.logger.warning(f"OSError. {e}. This message is not executable in the server environment. ({msg})", exc_info=True)
|
|
196
|
+
if msg is not None and len(msg) > 1:
|
|
197
|
+
self.redis_cli.rpush(msg[1], dict(warn=f"OSError. {e}. This message is not executable in the server environment. ({msg[0]})"))
|
|
198
|
+
error_cnt += 1
|
|
199
|
+
self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
|
|
200
|
+
pass
|
|
201
|
+
except IndexError as e:
|
|
202
|
+
self.logger.warning(f"IndexError. {e}. The message received by the server is invalid. ({msg})", exc_info=True)
|
|
203
|
+
if msg is not None and len(msg) > 1:
|
|
204
|
+
self.redis_cli.rpush(msg[1], dict(warn=f"IndexError. {e}. The message received by the server is invalid. ({msg[0]})"))
|
|
205
|
+
error_cnt += 1
|
|
206
|
+
self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
|
|
195
207
|
pass
|
|
196
208
|
except KeyboardInterrupt as e:
|
|
197
209
|
self.is_running = False
|
|
198
210
|
break
|
|
199
211
|
except Exception as e:
|
|
200
|
-
self.logger.warning(f"Unknown error occurred. {e}", exc_info=True)
|
|
212
|
+
self.logger.warning(f"Unknown error occurred. {e}. Service will be stopped due to unknown cause.({msg})", exc_info=True)
|
|
201
213
|
self.is_running = False
|
|
202
214
|
break
|
|
203
215
|
self.redis_cli.delete(self.redis_cli.svname)
|