cmdbox 0.5.4__py3-none-any.whl → 0.6.0.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.
- cmdbox/app/auth/signin.py +463 -303
- cmdbox/app/common.py +51 -3
- cmdbox/app/commons/loghandler.py +62 -13
- cmdbox/app/edge.py +5 -173
- cmdbox/app/edge_tool.py +177 -0
- cmdbox/app/feature.py +10 -9
- cmdbox/app/features/cli/agent_base.py +479 -0
- cmdbox/app/features/cli/audit_base.py +17 -5
- cmdbox/app/features/cli/cmdbox_audit_search.py +24 -1
- cmdbox/app/features/cli/cmdbox_client_file_download.py +1 -1
- cmdbox/app/features/cli/cmdbox_cmd_list.py +105 -0
- cmdbox/app/features/cli/cmdbox_cmd_load.py +104 -0
- cmdbox/app/features/cli/cmdbox_edge_config.py +2 -2
- cmdbox/app/features/cli/cmdbox_edge_start.py +1 -1
- cmdbox/app/features/cli/cmdbox_gui_start.py +9 -132
- cmdbox/app/features/cli/cmdbox_gui_stop.py +4 -21
- cmdbox/app/features/cli/cmdbox_server_start.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_genpass.py +0 -3
- 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 +120 -104
- cmdbox/app/features/cli/cmdbox_web_stop.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_add.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_edit.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_list.py +1 -1
- cmdbox/app/features/web/cmdbox_web_agent.py +262 -0
- cmdbox/app/features/web/cmdbox_web_do_signout.py +3 -3
- cmdbox/app/features/web/cmdbox_web_exec_cmd.py +8 -3
- cmdbox/app/features/web/cmdbox_web_signin.py +5 -4
- cmdbox/app/features/web/cmdbox_web_users.py +2 -0
- cmdbox/app/options.py +62 -9
- cmdbox/app/web.py +139 -15
- cmdbox/extensions/features.yml +18 -0
- cmdbox/extensions/sample_project/sample/app/features/cli/__init__.py +0 -0
- cmdbox/extensions/sample_project/sample/app/features/web/__init__.py +0 -0
- cmdbox/extensions/sample_project/sample/extensions/features.yml +18 -0
- cmdbox/extensions/sample_project/sample/extensions/user_list.yml +2 -1
- cmdbox/extensions/sample_project/sample/logconf_sample.yml +14 -1
- cmdbox/extensions/user_list.yml +1 -0
- cmdbox/licenses/LICENSE.Authlib.1.5.2(BSD License).txt +29 -0
- cmdbox/licenses/LICENSE.Deprecated.1.2.18(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.SQLAlchemy.2.0.40(MIT License).txt +19 -0
- cmdbox/licenses/LICENSE.aiohappyeyeballs.2.6.1(Python Software Foundation License).txt +279 -0
- cmdbox/licenses/LICENSE.aiohttp.3.11.18(Apache Software License).txt +13 -0
- cmdbox/licenses/LICENSE.aiosignal.1.3.2(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.attrs.25.3.0(UNKNOWN).txt +21 -0
- cmdbox/licenses/LICENSE.cachetools.5.5.2(MIT License).txt +20 -0
- cmdbox/licenses/LICENSE.distro.1.9.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.docstring_parser.0.16(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.filelock.3.18.0(The Unlicense (Unlicense)).txt +24 -0
- cmdbox/licenses/LICENSE.frozenlist.1.6.0(Apache-2.0).txt +201 -0
- cmdbox/licenses/LICENSE.fsspec.2025.3.2(BSD License).txt +29 -0
- cmdbox/licenses/LICENSE.google-adk.0.5.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-api-python-client.2.169.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.google-auth-httplib2.0.2.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.google-auth.2.40.1(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.google-cloud-aiplatform.1.92.0(Apache 2.0).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-bigquery.3.31.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-core.2.4.3(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-resource-manager.1.14.2(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-secret-manager.2.23.3(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-speech.2.32.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-storage.2.19.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-trace.1.16.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-crc32c.1.7.1(Apache 2.0).txt +202 -0
- cmdbox/licenses/LICENSE.google-genai.1.14.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-resumable-media.2.7.2(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.googleapis-common-protos.1.70.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.graphviz.0.20.3(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.grpc-google-iam-v1.0.14.2(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.grpcio-status.1.71.0(Apache Software License).txt +610 -0
- cmdbox/licenses/LICENSE.grpcio.1.71.0(Apache Software License).txt +610 -0
- cmdbox/licenses/LICENSE.httpcore.1.0.9(BSD License).txt +27 -0
- cmdbox/licenses/LICENSE.httplib2.0.22.0(MIT License).txt +23 -0
- cmdbox/licenses/LICENSE.httpx-sse.0.4.0(MIT).txt +21 -0
- cmdbox/licenses/LICENSE.httpx.0.28.1(BSD License).txt +12 -0
- cmdbox/licenses/LICENSE.huggingface-hub.0.31.1(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.importlib_metadata.8.6.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.jiter.0.9.0(MIT License).txt +1 -0
- cmdbox/licenses/LICENSE.jsonschema-specifications.2025.4.1(UNKNOWN).txt +19 -0
- cmdbox/licenses/LICENSE.jsonschema.4.23.0(MIT License).txt +19 -0
- cmdbox/licenses/LICENSE.litellm.1.69.0(MIT License).txt +26 -0
- cmdbox/licenses/LICENSE.mcp.1.8.0(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.multidict.6.4.3(Apache Software License).txt +13 -0
- cmdbox/licenses/LICENSE.openai.1.75.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-api.1.33.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-exporter-gcp-trace.1.9.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-resourcedetector-gcp.1.9.0a0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-sdk.1.33.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-semantic-conventions.0.54b0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.propcache.0.3.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.proto-plus.1.26.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.protobuf.5.29.4(3-Clause BSD License).txt +32 -0
- cmdbox/licenses/LICENSE.pyasn1.0.6.1(BSD License).txt +24 -0
- cmdbox/licenses/LICENSE.pyasn1_modules.0.4.2(BSD License).txt +24 -0
- cmdbox/licenses/LICENSE.pydantic-settings.2.9.1(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.pyparsing.3.2.3(MIT License).txt +18 -0
- cmdbox/licenses/LICENSE.python-dateutil.2.9.0.post0(Apache Software License; BSD License).txt +54 -0
- cmdbox/licenses/LICENSE.referencing.0.36.2(UNKNOWN).txt +19 -0
- cmdbox/licenses/LICENSE.regex.2024.11.6(Apache Software License).txt +208 -0
- cmdbox/licenses/LICENSE.rpds-py.0.24.0(MIT).txt +19 -0
- cmdbox/licenses/LICENSE.rsa.4.9.1(Apache Software License).txt +13 -0
- cmdbox/licenses/LICENSE.shapely.2.1.0(BSD License).txt +29 -0
- cmdbox/licenses/LICENSE.sse-starlette.2.3.4(BSD License).txt +27 -0
- cmdbox/licenses/LICENSE.tiktoken.0.9.0(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.tokenizers.0.21.1(Apache Software License).txt +1 -0
- cmdbox/licenses/LICENSE.tqdm.4.67.1(MIT License; Mozilla Public License 2.0 (MPL 2.0)).txt +49 -0
- cmdbox/licenses/LICENSE.tzlocal.5.3.1(MIT License).txt +19 -0
- cmdbox/licenses/LICENSE.uritemplate.4.1.1(Apache Software License; BSD License).txt +3 -0
- cmdbox/licenses/LICENSE.wrapt.1.17.2(BSD License).txt +24 -0
- cmdbox/licenses/LICENSE.yarl.1.20.0(Apache Software License).txt +202 -0
- cmdbox/licenses/files.txt +104 -11
- cmdbox/logconf_audit.yml +16 -3
- cmdbox/logconf_client.yml +16 -3
- cmdbox/logconf_cmdbox.yml +16 -3
- cmdbox/logconf_edge.yml +16 -3
- cmdbox/logconf_gui.yml +15 -2
- cmdbox/logconf_server.yml +15 -2
- cmdbox/logconf_web.yml +15 -2
- cmdbox/version.py +3 -2
- cmdbox/web/agent.html +263 -0
- cmdbox/web/assets/cmdbox/agent.js +338 -0
- cmdbox/web/assets/cmdbox/common.js +1111 -1020
- cmdbox/web/assets/cmdbox/main.js +17 -3
- cmdbox/web/assets/cmdbox/signin.js +4 -4
- cmdbox/web/assets/filer/filer.js +4 -2
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/METADATA +69 -26
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/RECORD +148 -67
- /cmdbox/licenses/{LICENSE.charset-normalizer.3.4.1(MIT License).txt → LICENSE.charset-normalizer.3.4.2(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.click.8.1.8(BSD License).txt → LICENSE.click.8.2.0(UNKNOWN).txt} +0 -0
- /cmdbox/licenses/{LICENSE.cryptography.44.0.2(Apache Software License; BSD License).txt → LICENSE.cryptography.44.0.3(Apache Software License; BSD License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.importlib_metadata.8.7.0(Apache Software License).txt → LICENSE.google-api-core.2.24.2(Apache Software License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.greenlet.3.2.1(MIT AND Python-2.0).txt → LICENSE.greenlet.3.2.2(MIT AND Python-2.0).txt} +0 -0
- /cmdbox/licenses/{LICENSE.psycopg-binary.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt → LICENSE.psycopg-binary.3.2.7(GNU Lesser General Public License v3 (LGPLv3)).txt} +0 -0
- /cmdbox/licenses/{LICENSE.psycopg.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt → LICENSE.psycopg.3.2.7(GNU Lesser General Public License v3 (LGPLv3)).txt} +0 -0
- /cmdbox/licenses/{LICENSE.pydantic.2.11.3(MIT License).txt → LICENSE.pydantic.2.11.4(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.pydantic_core.2.33.1(MIT License).txt → LICENSE.pydantic_core.2.33.2(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.redis.5.2.1(MIT License).txt → LICENSE.redis.6.0.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.snowballstemmer.2.2.0(BSD License).txt → LICENSE.snowballstemmer.3.0.1(BSD License).txt} +0 -0
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/LICENSE +0 -0
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/WHEEL +0 -0
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/entry_points.txt +0 -0
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/top_level.txt +0 -0
cmdbox/app/auth/signin.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from cmdbox.app import common, options
|
|
2
|
-
from fastapi import Request, Response, HTTPException
|
|
2
|
+
from fastapi import Request, Response, HTTPException, WebSocket
|
|
3
3
|
from fastapi.responses import RedirectResponse
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Dict, Any, Tuple, List, Union
|
|
6
6
|
import copy
|
|
7
|
+
import contextvars
|
|
7
8
|
import logging
|
|
8
9
|
import string
|
|
9
10
|
|
|
@@ -59,7 +60,8 @@ class Signin(object):
|
|
|
59
60
|
gids = [g['gid'] for g in copy_signin_data['groups'] if g['name'] in group_names]
|
|
60
61
|
return group_names, gids
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
@classmethod
|
|
64
|
+
def _enable_cors(cls, req:Request, res:Response) -> None:
|
|
63
65
|
"""
|
|
64
66
|
CORSを有効にする
|
|
65
67
|
|
|
@@ -71,7 +73,7 @@ class Signin(object):
|
|
|
71
73
|
return
|
|
72
74
|
res.headers['Access-Control-Allow-Origin'] = res.headers['Origin']
|
|
73
75
|
|
|
74
|
-
def check_signin(self, req:Request, res:Response):
|
|
76
|
+
def check_signin(self, req:Request, res:Response) -> Union[None, RedirectResponse]:
|
|
75
77
|
"""
|
|
76
78
|
サインインをチェックする
|
|
77
79
|
|
|
@@ -80,24 +82,44 @@ class Signin(object):
|
|
|
80
82
|
res (Response): レスポンス
|
|
81
83
|
|
|
82
84
|
Returns:
|
|
83
|
-
|
|
85
|
+
Union[None, RedirectResponse]: サインインエラーの場合はリダイレクトレスポンス
|
|
84
86
|
"""
|
|
85
|
-
self.enable_cors(req, res)
|
|
86
87
|
if self.signin_file_data is None:
|
|
87
88
|
return None
|
|
88
89
|
if 'signin' in req.session:
|
|
89
|
-
self.signin_file_data =
|
|
90
|
-
|
|
90
|
+
self.signin_file_data = Signin.load_signin_file(self.signin_file, self.signin_file_data) # サインインファイルの更新をチェック
|
|
91
|
+
return Signin._check_signin(req, res, self.signin_file_data, self.logger)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def _check_signin(cls, req:Request, res:Response, signin_file_data:Dict[str, Any], logger:logging.Logger) -> Union[None, RedirectResponse]:
|
|
95
|
+
"""
|
|
96
|
+
サインインをチェックする
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
req (Request): リクエスト
|
|
100
|
+
res (Response): レスポンス
|
|
101
|
+
signin_file_data (Dict[str, Any]): サインインファイルデータ(変更不可)
|
|
102
|
+
logger (logging.Logger): ロガー
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Union[None, RedirectResponse]: サインインエラーの場合はリダイレクトレスポンス
|
|
106
|
+
"""
|
|
107
|
+
Signin._enable_cors(req, res)
|
|
108
|
+
if signin_file_data is None:
|
|
109
|
+
return None
|
|
110
|
+
if 'signin' in req.session:
|
|
111
|
+
path_jadge = Signin._check_path(req, req.url.path, signin_file_data, logger)
|
|
91
112
|
if path_jadge is not None:
|
|
92
113
|
return path_jadge
|
|
93
114
|
return None
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
if logger.level == logging.DEBUG:
|
|
116
|
+
logger.debug(f"Not found siginin session. Try check_apikey. path={req.url.path}")
|
|
117
|
+
ret = Signin._check_apikey(req, res, signin_file_data, logger)
|
|
118
|
+
if ret is not None and logger.level == logging.DEBUG:
|
|
119
|
+
logger.debug(f"Not signed in.")
|
|
98
120
|
return ret
|
|
99
121
|
|
|
100
|
-
def check_apikey(self, req:Request, res:Response):
|
|
122
|
+
def check_apikey(self, req:Request, res:Response) -> Union[None, RedirectResponse]:
|
|
101
123
|
"""
|
|
102
124
|
ApiKeyをチェックする
|
|
103
125
|
|
|
@@ -106,56 +128,71 @@ class Signin(object):
|
|
|
106
128
|
res (Response): レスポンス
|
|
107
129
|
|
|
108
130
|
Returns:
|
|
109
|
-
|
|
131
|
+
Union[None, RedirectResponse]: サインインエラーの場合はリダイレクトレスポンス
|
|
110
132
|
"""
|
|
111
|
-
|
|
112
|
-
|
|
133
|
+
return Signin._check_apikey(req, res, self.signin_file_data)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def _check_apikey(cls, req:Request, res:Response, signin_file_data:Dict[str, Any], logger:logging.Logger) -> Union[None, RedirectResponse]:
|
|
137
|
+
"""
|
|
138
|
+
ApiKeyをチェックする
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
req (Request): リクエスト
|
|
142
|
+
res (Response): レスポンス
|
|
143
|
+
signin_file_data (Dict[str, Any]): サインインファイルデータ(変更不可)
|
|
144
|
+
logger (logging.Logger): ロガー
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Union[None, RedirectResponse]: サインインエラーの場合はリダイレクトレスポンス
|
|
148
|
+
"""
|
|
149
|
+
Signin._enable_cors(req, res)
|
|
150
|
+
if signin_file_data is None:
|
|
113
151
|
res.headers['signin'] = 'success'
|
|
114
152
|
return None
|
|
115
153
|
if 'Authorization' not in req.headers:
|
|
116
|
-
self.logger.warning(f"Authorization not found. headers={req.headers}")
|
|
154
|
+
#self.logger.warning(f"Authorization not found. headers={req.headers}")
|
|
117
155
|
return RedirectResponse(url=f'/signin{req.url.path}?error=noauth')
|
|
118
156
|
auth = req.headers['Authorization']
|
|
119
157
|
if not auth.startswith('Bearer '):
|
|
120
|
-
self.logger.warning(f"Bearer not found. headers={req.headers}")
|
|
158
|
+
#self.logger.warning(f"Bearer not found. headers={req.headers}")
|
|
121
159
|
return RedirectResponse(url=f'/signin{req.url.path}?error=apikeyfail')
|
|
122
160
|
bearer, apikey = auth.split(' ')
|
|
123
161
|
apikey = common.hash_password(apikey.strip(), 'sha1')
|
|
124
|
-
if
|
|
125
|
-
|
|
162
|
+
if logger.level == logging.DEBUG:
|
|
163
|
+
logger.debug(f"hashed apikey: {apikey}")
|
|
126
164
|
find_user = None
|
|
127
|
-
|
|
128
|
-
for user in self.signin_file_data['users']:
|
|
165
|
+
for user in signin_file_data['users']:
|
|
129
166
|
if 'apikeys' not in user:
|
|
130
167
|
continue
|
|
131
168
|
for ak, key in user['apikeys'].items():
|
|
132
169
|
if apikey == key:
|
|
133
170
|
find_user = user
|
|
134
171
|
if find_user is None:
|
|
135
|
-
|
|
172
|
+
logger.warning(f"No matching user found for apikey.")
|
|
136
173
|
return RedirectResponse(url=f'/signin{req.url.path}?error=apikeyfail')
|
|
137
174
|
|
|
138
|
-
group_names = list(set(
|
|
139
|
-
gids = [g['gid'] for g in
|
|
175
|
+
group_names = list(set(Signin.correct_group(signin_file_data, find_user['groups'], None)))
|
|
176
|
+
gids = [g['gid'] for g in signin_file_data['groups'] if g['name'] in group_names]
|
|
140
177
|
req.session['signin'] = dict(uid=find_user['uid'], name=find_user['name'], password=find_user['password'],
|
|
141
178
|
gids=gids, groups=group_names)
|
|
142
|
-
if
|
|
143
|
-
|
|
179
|
+
if logger.level == logging.DEBUG:
|
|
180
|
+
logger.debug(f"find user: name={find_user['name']}, group_names={group_names}")
|
|
144
181
|
# パスルールチェック
|
|
145
182
|
user_groups = find_user['groups']
|
|
146
|
-
jadge =
|
|
147
|
-
for rule in
|
|
183
|
+
jadge = signin_file_data['pathrule']['policy']
|
|
184
|
+
for rule in signin_file_data['pathrule']['rules']:
|
|
148
185
|
if len([g for g in rule['groups'] if g in user_groups]) <= 0:
|
|
149
186
|
continue
|
|
150
187
|
if len([p for p in rule['paths'] if req.url.path.startswith(p)]) <= 0:
|
|
151
188
|
continue
|
|
152
189
|
jadge = rule['rule']
|
|
153
|
-
if
|
|
154
|
-
|
|
190
|
+
if logger.level == logging.DEBUG:
|
|
191
|
+
logger.debug(f"rule: {req.url.path}: {jadge}")
|
|
155
192
|
if jadge == 'allow':
|
|
156
193
|
res.headers['signin'] = 'success'
|
|
157
194
|
return None
|
|
158
|
-
|
|
195
|
+
logger.warning(f"Unauthorized site. user={find_user['name']}, path={req.url.path}")
|
|
159
196
|
return RedirectResponse(url=f'/signin{req.url.path}?error=unauthorizedsite')
|
|
160
197
|
|
|
161
198
|
@classmethod
|
|
@@ -173,268 +210,270 @@ class Signin(object):
|
|
|
173
210
|
Returns:
|
|
174
211
|
Dict[str, Any]: サインインファイルデータ
|
|
175
212
|
"""
|
|
176
|
-
if signin_file is
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return signin_file_data
|
|
213
|
+
if signin_file is None:
|
|
214
|
+
return None
|
|
215
|
+
signin_file = Path(signin_file) if isinstance(signin_file, str) else signin_file
|
|
216
|
+
if not signin_file.is_file():
|
|
217
|
+
raise HTTPException(status_code=500, detail=f'signin_file is not found. ({signin_file})')
|
|
218
|
+
# サインインファイル読込み済みなら返すが、別プロセスがサインインファイルを更新していたら読込みを実施する。
|
|
219
|
+
if not hasattr(cls, 'signin_file_last'):
|
|
184
220
|
cls.signin_file_last = signin_file.stat().st_mtime
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if '
|
|
263
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if '
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if '
|
|
356
|
-
raise HTTPException(status_code=500, detail=f'signin_file format error. "
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
221
|
+
if cls.signin_file_last >= signin_file.stat().st_mtime and signin_file_data is not None:
|
|
222
|
+
return signin_file_data
|
|
223
|
+
cls.signin_file_last = signin_file.stat().st_mtime
|
|
224
|
+
yml = common.load_yml(signin_file)
|
|
225
|
+
# usersのフォーマットチェック
|
|
226
|
+
if 'users' not in yml:
|
|
227
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "users" not found. ({signin_file})')
|
|
228
|
+
uids = set()
|
|
229
|
+
unames = set()
|
|
230
|
+
groups = [g['name'] for g in yml['groups']]
|
|
231
|
+
for user in yml['users']:
|
|
232
|
+
if 'uid' not in user or user['uid'] is None:
|
|
233
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "uid" not found or empty. ({signin_file})')
|
|
234
|
+
if user['uid'] in uids:
|
|
235
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate uid found. ({signin_file}). uid={user["uid"]}')
|
|
236
|
+
if 'name' not in user or user['name'] is None:
|
|
237
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "name" not found or empty. ({signin_file})')
|
|
238
|
+
if user['name'] in unames:
|
|
239
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate name found. ({signin_file}). name={user["name"]}')
|
|
240
|
+
if 'password' not in user:
|
|
241
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "password" not found or empty. ({signin_file})')
|
|
242
|
+
if 'hash' not in user or user['hash'] is None:
|
|
243
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "hash" not found or empty. ({signin_file})')
|
|
244
|
+
if user['hash'] not in ['oauth2', 'saml', 'plain', 'md5', 'sha1', 'sha256']:
|
|
245
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. Algorithms not supported. ({signin_file}). hash={user["hash"]} "oauth2", "saml", "plain", "md5", "sha1", "sha256" only.')
|
|
246
|
+
if 'groups' not in user or type(user['groups']) is not list:
|
|
247
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found or not list type. ({signin_file})')
|
|
248
|
+
if len([ug for ug in user['groups'] if ug not in groups]) > 0:
|
|
249
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. Group not found. ({signin_file}). {user["groups"]}')
|
|
250
|
+
uids.add(user['uid'])
|
|
251
|
+
unames.add(user['name'])
|
|
252
|
+
# groupsのフォーマットチェック
|
|
253
|
+
if 'groups' not in yml:
|
|
254
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found. ({signin_file})')
|
|
255
|
+
gids = set()
|
|
256
|
+
gnames = set()
|
|
257
|
+
for group in yml['groups']:
|
|
258
|
+
if 'gid' not in group or group['gid'] is None:
|
|
259
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "gid" not found or empty. ({signin_file})')
|
|
260
|
+
if group['gid'] in gids:
|
|
261
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate gid found. ({signin_file}). gid={group["gid"]}')
|
|
262
|
+
if 'name' not in group or group['name'] is None:
|
|
263
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "name" not found or empty. ({signin_file})')
|
|
264
|
+
if group['name'] in gnames:
|
|
265
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. Duplicate name found. ({signin_file}). name={group["name"]}')
|
|
266
|
+
if 'parent' in group:
|
|
267
|
+
if group['parent'] not in groups:
|
|
268
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. Parent group not found. ({signin_file}). parent={group["parent"]}')
|
|
269
|
+
gids.add(group['gid'])
|
|
270
|
+
gnames.add(group['name'])
|
|
271
|
+
# cmdruleのフォーマットチェック
|
|
272
|
+
if 'cmdrule' not in yml:
|
|
273
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "cmdrule" not found. ({signin_file})')
|
|
274
|
+
if 'policy' not in yml['cmdrule']:
|
|
275
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "cmdrule". ({signin_file})')
|
|
276
|
+
if yml['cmdrule']['policy'] not in ['allow', 'deny']:
|
|
277
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not supported in "cmdrule". ({signin_file}). "allow" or "deny" only.')
|
|
278
|
+
if 'rules' not in yml['cmdrule']:
|
|
279
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not found in "cmdrule". ({signin_file})')
|
|
280
|
+
if type(yml['cmdrule']['rules']) is not list:
|
|
281
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not list type in "cmdrule". ({signin_file})')
|
|
282
|
+
for rule in yml['cmdrule']['rules']:
|
|
283
|
+
if 'groups' not in rule:
|
|
284
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found in "cmdrule.rules" ({signin_file})')
|
|
285
|
+
if type(rule['groups']) is not list:
|
|
286
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not list type in "cmdrule.rules". ({signin_file})')
|
|
287
|
+
rule['groups'] = list(set(copy.deepcopy(cls.correct_group(yml, rule['groups'], yml['groups']))))
|
|
288
|
+
if 'rule' not in rule:
|
|
289
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not found in "cmdrule.rules" ({signin_file})')
|
|
290
|
+
if rule['rule'] not in ['allow', 'deny']:
|
|
291
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not supported in "cmdrule.rules". ({signin_file}). "allow" or "deny" only.')
|
|
292
|
+
if 'mode' not in rule:
|
|
293
|
+
rule['mode'] = None
|
|
294
|
+
if 'cmds' not in rule:
|
|
295
|
+
rule['cmds'] = []
|
|
296
|
+
if rule['mode'] is None and len(rule['cmds']) > 0:
|
|
297
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. When “cmds” is specified, “mode” must be specified. ({signin_file})')
|
|
298
|
+
if type(rule['cmds']) is not list:
|
|
299
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "cmds" not list type in "cmdrule.rules". ({signin_file})')
|
|
300
|
+
# pathruleのフォーマットチェック
|
|
301
|
+
if 'pathrule' not in yml:
|
|
302
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "pathrule" not found. ({signin_file})')
|
|
303
|
+
if 'policy' not in yml['pathrule']:
|
|
304
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "pathrule". ({signin_file})')
|
|
305
|
+
if yml['pathrule']['policy'] not in ['allow', 'deny']:
|
|
306
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not supported in "pathrule". ({signin_file}). "allow" or "deny" only.')
|
|
307
|
+
if 'rules' not in yml['pathrule']:
|
|
308
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not found in "pathrule". ({signin_file})')
|
|
309
|
+
if type(yml['pathrule']['rules']) is not list:
|
|
310
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "rules" not list type in "pathrule". ({signin_file})')
|
|
311
|
+
for rule in yml['pathrule']['rules']:
|
|
312
|
+
if 'groups' not in rule:
|
|
313
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not found in "pathrule.rules" ({signin_file})')
|
|
314
|
+
if type(rule['groups']) is not list:
|
|
315
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "groups" not list type in "pathrule.rules". ({signin_file})')
|
|
316
|
+
rule['groups'] = list(set(copy.deepcopy(cls.correct_group(yml, rule['groups'], yml['groups']))))
|
|
317
|
+
if 'rule' not in rule:
|
|
318
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not found in "pathrule.rules" ({signin_file})')
|
|
319
|
+
if rule['rule'] not in ['allow', 'deny']:
|
|
320
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "rule" not supported in "pathrule.rules". ({signin_file}). "allow" or "deny" only.')
|
|
321
|
+
if 'paths' not in rule:
|
|
322
|
+
rule['paths'] = []
|
|
323
|
+
if type(rule['paths']) is not list:
|
|
324
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "paths" not list type in "pathrule.rules". ({signin_file})')
|
|
325
|
+
# passwordのフォーマットチェック
|
|
326
|
+
if 'password' in yml:
|
|
327
|
+
if 'policy' not in yml['password']:
|
|
328
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "policy" not found in "password". ({signin_file})')
|
|
329
|
+
if 'enabled' not in yml['password']['policy']:
|
|
330
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.policy". ({signin_file})')
|
|
331
|
+
if type(yml['password']['policy']['enabled']) is not bool:
|
|
332
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.policy". ({signin_file})')
|
|
333
|
+
if 'not_same_before' not in yml['password']['policy']:
|
|
334
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not found in "password.policy". ({signin_file})')
|
|
335
|
+
if type(yml['password']['policy']['not_same_before']) is not bool:
|
|
336
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_same_before" not bool type in "password.policy". ({signin_file})')
|
|
337
|
+
if 'min_length' not in yml['password']['policy']:
|
|
338
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not found in "password.policy". ({signin_file})')
|
|
339
|
+
if type(yml['password']['policy']['min_length']) is not int:
|
|
340
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_length" not int type in "password.policy". ({signin_file})')
|
|
341
|
+
if 'max_length' not in yml['password']['policy']:
|
|
342
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not found in "password.policy". ({signin_file})')
|
|
343
|
+
if type(yml['password']['policy']['max_length']) is not int:
|
|
344
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "max_length" not int type in "password.policy". ({signin_file})')
|
|
345
|
+
if 'min_lowercase' not in yml['password']['policy']:
|
|
346
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not found in "password.policy". ({signin_file})')
|
|
347
|
+
if type(yml['password']['policy']['min_lowercase']) is not int:
|
|
348
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_lowercase" not int type in "password.policy". ({signin_file})')
|
|
349
|
+
if 'min_uppercase' not in yml['password']['policy']:
|
|
350
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not found in "password.policy". ({signin_file})')
|
|
351
|
+
if type(yml['password']['policy']['min_uppercase']) is not int:
|
|
352
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_uppercase" not int type in "password.policy". ({signin_file})')
|
|
353
|
+
if 'min_digit' not in yml['password']['policy']:
|
|
354
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not found in "password.policy". ({signin_file})')
|
|
355
|
+
if type(yml['password']['policy']['min_digit']) is not int:
|
|
356
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_digit" not int type in "password.policy". ({signin_file})')
|
|
357
|
+
if 'min_symbol' not in yml['password']['policy']:
|
|
358
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not found in "password.policy". ({signin_file})')
|
|
359
|
+
if type(yml['password']['policy']['min_symbol']) is not int:
|
|
360
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "min_symbol" not int type in "password.policy". ({signin_file})')
|
|
361
|
+
if 'not_contain_username' not in yml['password']['policy']:
|
|
362
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not found in "password.policy". ({signin_file})')
|
|
363
|
+
if type(yml['password']['policy']['not_contain_username']) is not bool:
|
|
364
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "not_contain_username" not bool type in "password.policy". ({signin_file})')
|
|
365
|
+
if 'expiration' not in yml['password']:
|
|
366
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "expiration" not found in "password". ({signin_file})')
|
|
367
|
+
if 'enabled' not in yml['password']['expiration']:
|
|
368
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.expiration". ({signin_file})')
|
|
369
|
+
if type(yml['password']['expiration']['enabled']) is not bool:
|
|
370
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.expiration". ({signin_file})')
|
|
371
|
+
if 'period' not in yml['password']['expiration']:
|
|
372
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not found in "password.expiration". ({signin_file})')
|
|
373
|
+
if type(yml['password']['expiration']['period']) is not int:
|
|
374
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "period" not int type in "password.expiration". ({signin_file})')
|
|
375
|
+
if 'notify' not in yml['password']['expiration']:
|
|
376
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not found in "password.expiration". ({signin_file})')
|
|
377
|
+
if type(yml['password']['expiration']['notify']) is not int:
|
|
378
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "notify" not int type in "password.expiration". ({signin_file})')
|
|
379
|
+
if 'lockout' not in yml['password']:
|
|
380
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "lockout" not found in "password". ({signin_file})')
|
|
381
|
+
if 'enabled' not in yml['password']['lockout']:
|
|
382
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "password.lockout". ({signin_file})')
|
|
383
|
+
if type(yml['password']['lockout']['enabled']) is not bool:
|
|
384
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "password.lockout". ({signin_file})')
|
|
385
|
+
if 'threshold' not in yml['password']['lockout']:
|
|
386
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not found in "password.lockout". ({signin_file})')
|
|
387
|
+
if type(yml['password']['lockout']['threshold']) is not int:
|
|
388
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "threshold" not int type in "password.lockout". ({signin_file})')
|
|
389
|
+
if 'reset' not in yml['password']['lockout']:
|
|
390
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not found in "password.lockout". ({signin_file})')
|
|
391
|
+
if type(yml['password']['lockout']['reset']) is not int:
|
|
392
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "reset" not int type in "password.lockout". ({signin_file})')
|
|
393
|
+
# oauth2のフォーマットチェック
|
|
394
|
+
if 'oauth2' not in yml:
|
|
395
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "oauth2" not found. ({signin_file})')
|
|
396
|
+
if 'providers' not in yml['oauth2']:
|
|
397
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "providers" not found in "oauth2". ({signin_file})')
|
|
398
|
+
# google
|
|
399
|
+
if 'google' not in yml['oauth2']['providers']:
|
|
400
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "google" not found in "providers". ({signin_file})')
|
|
401
|
+
if 'enabled' not in yml['oauth2']['providers']['google']:
|
|
402
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "google". ({signin_file})')
|
|
403
|
+
if type(yml['oauth2']['providers']['google']['enabled']) is not bool:
|
|
404
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "google". ({signin_file})')
|
|
405
|
+
if 'client_id' not in yml['oauth2']['providers']['google']:
|
|
406
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "google". ({signin_file})')
|
|
407
|
+
if 'client_secret' not in yml['oauth2']['providers']['google']:
|
|
408
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "google". ({signin_file})')
|
|
409
|
+
if 'redirect_uri' not in yml['oauth2']['providers']['google']:
|
|
410
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "google". ({signin_file})')
|
|
411
|
+
if 'scope' not in yml['oauth2']['providers']['google']:
|
|
412
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "google". ({signin_file})')
|
|
413
|
+
if type(yml['oauth2']['providers']['google']['scope']) is not list:
|
|
414
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "google". ({signin_file})')
|
|
415
|
+
if 'signin_module' not in yml['oauth2']['providers']['google']:
|
|
416
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "google". ({signin_file})')
|
|
417
|
+
# github
|
|
418
|
+
if 'github' not in yml['oauth2']['providers']:
|
|
419
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "github" not found in "providers". ({signin_file})')
|
|
420
|
+
if 'enabled' not in yml['oauth2']['providers']['github']:
|
|
421
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "github". ({signin_file})')
|
|
422
|
+
if type(yml['oauth2']['providers']['github']['enabled']) is not bool:
|
|
423
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "github". ({signin_file})')
|
|
424
|
+
if 'client_id' not in yml['oauth2']['providers']['github']:
|
|
425
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "github". ({signin_file})')
|
|
426
|
+
if 'client_secret' not in yml['oauth2']['providers']['github']:
|
|
427
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "github". ({signin_file})')
|
|
428
|
+
if 'redirect_uri' not in yml['oauth2']['providers']['github']:
|
|
429
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "github". ({signin_file})')
|
|
430
|
+
if 'scope' not in yml['oauth2']['providers']['github']:
|
|
431
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "github". ({signin_file})')
|
|
432
|
+
if type(yml['oauth2']['providers']['github']['scope']) is not list:
|
|
433
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "github". ({signin_file})')
|
|
434
|
+
if 'signin_module' not in yml['oauth2']['providers']['github']:
|
|
435
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "github". ({signin_file})')
|
|
436
|
+
# azure
|
|
437
|
+
if 'azure' not in yml['oauth2']['providers']:
|
|
438
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "azure" not found in "providers". ({signin_file})')
|
|
439
|
+
if 'enabled' not in yml['oauth2']['providers']['azure']:
|
|
440
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "azure". ({signin_file})')
|
|
441
|
+
if type(yml['oauth2']['providers']['azure']['enabled']) is not bool:
|
|
442
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "azure". ({signin_file})')
|
|
443
|
+
if 'tenant_id' not in yml['oauth2']['providers']['azure']:
|
|
444
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "tenant_id" not found in "azure". ({signin_file})')
|
|
445
|
+
if 'client_id' not in yml['oauth2']['providers']['azure']:
|
|
446
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_id" not found in "azure". ({signin_file})')
|
|
447
|
+
if 'client_secret' not in yml['oauth2']['providers']['azure']:
|
|
448
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "client_secret" not found in "azure". ({signin_file})')
|
|
449
|
+
if 'redirect_uri' not in yml['oauth2']['providers']['azure']:
|
|
450
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "redirect_uri" not found in "azure". ({signin_file})')
|
|
451
|
+
if 'scope' not in yml['oauth2']['providers']['azure']:
|
|
452
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not found in "azure". ({signin_file})')
|
|
453
|
+
if type(yml['oauth2']['providers']['azure']['scope']) is not list:
|
|
454
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "scope" not list type in "azure". ({signin_file})')
|
|
455
|
+
if 'signin_module' not in yml['oauth2']['providers']['azure']:
|
|
456
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "azure". ({signin_file})')
|
|
457
|
+
# samlのフォーマットチェック
|
|
458
|
+
if 'saml' not in yml:
|
|
459
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "saml" not found. ({signin_file})')
|
|
460
|
+
if 'providers' not in yml['saml']:
|
|
461
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "providers" not found in "saml". ({signin_file})')
|
|
462
|
+
# azure
|
|
463
|
+
if 'azure' not in yml['saml']['providers']:
|
|
464
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "azure" not found in "providers". ({signin_file})')
|
|
465
|
+
if 'enabled' not in yml['saml']['providers']['azure']:
|
|
466
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not found in "azure". ({signin_file})')
|
|
467
|
+
if type(yml['saml']['providers']['azure']['enabled']) is not bool:
|
|
468
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "enabled" not bool type in "azure". ({signin_file})')
|
|
469
|
+
if 'signin_module' not in yml['saml']['providers']['azure']:
|
|
470
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "signin_module" not found in "azure". ({signin_file})')
|
|
471
|
+
if 'sp' not in yml['saml']['providers']['azure']:
|
|
472
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "sp" not found in "azure". ({signin_file})')
|
|
473
|
+
if 'idp' not in yml['saml']['providers']['azure']:
|
|
474
|
+
raise HTTPException(status_code=500, detail=f'signin_file format error. "idp" not found in "azure". ({signin_file})')
|
|
475
|
+
# フォーマットチェックOK
|
|
476
|
+
return yml
|
|
438
477
|
|
|
439
478
|
@classmethod
|
|
440
479
|
def correct_group(cls, signin_file_data:Dict[str, Any], group_names:List[str], master_groups:List[Dict[str, Any]]) -> List[str]:
|
|
@@ -465,26 +504,42 @@ class Signin(object):
|
|
|
465
504
|
Returns:
|
|
466
505
|
Union[None, RedirectResponse]: 認可された場合はNone、認可されなかった場合はリダイレクトレスポンス
|
|
467
506
|
"""
|
|
468
|
-
|
|
507
|
+
return Signin._check_path(req, path, self.signin_file_data, self.logger)
|
|
508
|
+
|
|
509
|
+
@classmethod
|
|
510
|
+
def _check_path(cls, req:Request, path:str, signin_file_data:Dict[str, Any], logger:logging.Logger) -> Union[None, RedirectResponse]:
|
|
511
|
+
"""
|
|
512
|
+
パスの認可をチェックします
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
req (Request): リクエスト
|
|
516
|
+
path (str): パス
|
|
517
|
+
signin_file_data (Dict[str, Any]): サインインファイルデータ
|
|
518
|
+
logger (logging.Logger): ロガー
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
Union[None, RedirectResponse]: 認可された場合はNone、認可されなかった場合はリダイレクトレスポンス
|
|
522
|
+
"""
|
|
523
|
+
if signin_file_data is None:
|
|
469
524
|
return None
|
|
470
525
|
if 'signin' not in req.session:
|
|
471
526
|
return None
|
|
472
527
|
path = path if path.startswith('/') else f'/{path}'
|
|
473
528
|
# パスルールチェック
|
|
474
529
|
user_groups = req.session['signin']['groups']
|
|
475
|
-
jadge =
|
|
476
|
-
for rule in
|
|
530
|
+
jadge = signin_file_data['pathrule']['policy']
|
|
531
|
+
for rule in signin_file_data['pathrule']['rules']:
|
|
477
532
|
if len([g for g in rule['groups'] if g in user_groups]) <= 0:
|
|
478
533
|
continue
|
|
479
534
|
if len([p for p in rule['paths'] if path.startswith(p)]) <= 0:
|
|
480
535
|
continue
|
|
481
536
|
jadge = rule['rule']
|
|
482
|
-
if
|
|
483
|
-
|
|
537
|
+
if logger.level == logging.DEBUG:
|
|
538
|
+
logger.debug(f"rule: {path}: {jadge}")
|
|
484
539
|
if jadge == 'allow':
|
|
485
540
|
return None
|
|
486
541
|
else:
|
|
487
|
-
|
|
542
|
+
logger.warning(f"Unauthorized site. user={req.session['signin']['name']}, path={path}")
|
|
488
543
|
return RedirectResponse(url=f'/signin{path}?error=unauthorizedsite')
|
|
489
544
|
|
|
490
545
|
def check_cmd(self, req:Request, res:Response, mode:str, cmd:str):
|
|
@@ -504,10 +559,57 @@ class Signin(object):
|
|
|
504
559
|
return True
|
|
505
560
|
if 'signin' not in req.session or 'groups' not in req.session['signin']:
|
|
506
561
|
return False
|
|
562
|
+
return Signin._check_cmd(self.signin_file_data, req.session['signin']['groups'], mode, cmd, self.logger)
|
|
563
|
+
|
|
564
|
+
@classmethod
|
|
565
|
+
def load_groups(cls, signin_file_data:Dict[str, Any], apikey:str, logger:logging.Logger):
|
|
566
|
+
"""
|
|
567
|
+
APIキーからユーザグループを取得します
|
|
568
|
+
Args:
|
|
569
|
+
signin_file_data (Dict[str, Any]): サインインファイルデータ
|
|
570
|
+
apikey (str): APIキー
|
|
571
|
+
logger (logging.Logger): ロガー
|
|
572
|
+
Returns:
|
|
573
|
+
Dict[str, Any]: ユーザグループの情報
|
|
574
|
+
"""
|
|
575
|
+
apikey = common.hash_password(apikey.strip(), 'sha1')
|
|
576
|
+
if logger.level == logging.DEBUG:
|
|
577
|
+
logger.debug(f"hashed apikey: {apikey}")
|
|
578
|
+
find_user = None
|
|
579
|
+
for user in signin_file_data['users']:
|
|
580
|
+
if 'apikeys' not in user:
|
|
581
|
+
continue
|
|
582
|
+
for ak, key in user['apikeys'].items():
|
|
583
|
+
if apikey == key:
|
|
584
|
+
find_user = user
|
|
585
|
+
if find_user is None:
|
|
586
|
+
logger.warning(f"No matching user found for apikey.")
|
|
587
|
+
return dict(warn='No matching user found for apikey.')
|
|
588
|
+
|
|
589
|
+
group_names = list(set(Signin.correct_group(signin_file_data, find_user['groups'], None)))
|
|
590
|
+
return dict(success=group_names)
|
|
591
|
+
|
|
592
|
+
@classmethod
|
|
593
|
+
def _check_cmd(cls, signin_file_data:Dict[str, Any], user_groups:List[str], mode:str, cmd:str, logger:logging.Logger) -> bool:
|
|
594
|
+
"""
|
|
595
|
+
コマンドの認可をチェックします
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
signin_file_data (Dict[str, Any]): サインインファイルデータ
|
|
599
|
+
user_groups (List[str]): ユーザグループ
|
|
600
|
+
mode (str): モード
|
|
601
|
+
cmd (str): コマンド
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
bool: 認可されたかどうか
|
|
605
|
+
"""
|
|
606
|
+
if signin_file_data is None:
|
|
607
|
+
return True
|
|
608
|
+
if user_groups is None or len(user_groups) <= 0:
|
|
609
|
+
return False
|
|
507
610
|
# コマンドチェック
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
for rule in self.signin_file_data['cmdrule']['rules']:
|
|
611
|
+
jadge = signin_file_data['cmdrule']['policy']
|
|
612
|
+
for rule in signin_file_data['cmdrule']['rules']:
|
|
511
613
|
if len([g for g in rule['groups'] if g in user_groups]) <= 0:
|
|
512
614
|
continue
|
|
513
615
|
if rule['mode'] is not None:
|
|
@@ -516,8 +618,8 @@ class Signin(object):
|
|
|
516
618
|
if len([c for c in rule['cmds'] if cmd == c]) <= 0:
|
|
517
619
|
continue
|
|
518
620
|
jadge = rule['rule']
|
|
519
|
-
if
|
|
520
|
-
|
|
621
|
+
if logger.level == logging.DEBUG:
|
|
622
|
+
logger.debug(f"rule: mode={mode}, cmd={cmd}: {jadge}")
|
|
521
623
|
return jadge == 'allow'
|
|
522
624
|
|
|
523
625
|
def get_enable_modes(self, req:Request, res:Response) -> List[str]:
|
|
@@ -675,3 +777,61 @@ class Signin(object):
|
|
|
675
777
|
str: メールアドレス
|
|
676
778
|
"""
|
|
677
779
|
return self.__class__.get_email(data)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
request_scope = contextvars.ContextVar('request_scope', default=None)
|
|
783
|
+
|
|
784
|
+
async def create_request_scope(req:Request=None, res:Response=None, websocket:WebSocket=None):
|
|
785
|
+
"""
|
|
786
|
+
FastAPIのDepends用に、ContextVarを使用してリクエストスコープを提供します。
|
|
787
|
+
これにより、リクエストごとに異なるRequestオブジェクトを取得できます。
|
|
788
|
+
これは、FastAPIのDependsで使用されることを意図しています。
|
|
789
|
+
次のように使用します。
|
|
790
|
+
|
|
791
|
+
Example:
|
|
792
|
+
|
|
793
|
+
::
|
|
794
|
+
|
|
795
|
+
from cmdbox.app.auth import signin
|
|
796
|
+
from fastapi import Depends, Request, Response
|
|
797
|
+
|
|
798
|
+
@app.get("/some-endpoint")
|
|
799
|
+
async def some_endpoint(req: Request, res: Response, scope=Depends(signin.create_request_scope)):
|
|
800
|
+
# 何らかの処理
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
req (Request): リクエスト
|
|
804
|
+
res (Response): レスポンス
|
|
805
|
+
websocket (WebSocket): WebSocket接続
|
|
806
|
+
"""
|
|
807
|
+
sess = None
|
|
808
|
+
if req is not None:
|
|
809
|
+
sess = req.session if hasattr(req, 'session') else None
|
|
810
|
+
request_scope.set(dict(req=req, res=res, websocket=websocket))
|
|
811
|
+
try:
|
|
812
|
+
yield # リクエストの処理
|
|
813
|
+
finally:
|
|
814
|
+
# リクエストの処理が終わったら、ContextVarをクリアします
|
|
815
|
+
request_scope.set(None)
|
|
816
|
+
|
|
817
|
+
def get_request_scope() -> Dict[str, Any]:
|
|
818
|
+
"""
|
|
819
|
+
FastAPIのDepends用に、ContextVarからリクエストスコープを取得します。
|
|
820
|
+
|
|
821
|
+
Example:
|
|
822
|
+
|
|
823
|
+
::
|
|
824
|
+
|
|
825
|
+
from cmdbox.app.auth import signin
|
|
826
|
+
from fastapi import Request, Response
|
|
827
|
+
scope = signin.get_request_scope()
|
|
828
|
+
scope['req'] # Requestオブジェクト
|
|
829
|
+
scope['res'] # Responseオブジェクト
|
|
830
|
+
scope['session'] # sessionを表す辞書
|
|
831
|
+
scope['websocket'] # WebSocket接続
|
|
832
|
+
scope['logger'] # loggerオブジェクト
|
|
833
|
+
|
|
834
|
+
Returns:
|
|
835
|
+
Dict[str, Any]: リクエストとレスポンスとWebSocket接続
|
|
836
|
+
"""
|
|
837
|
+
return request_scope.get() if request_scope.get() is not None else dict(req=None, res=None, session=None, websocket=None)
|