diff --git a/ecs/jskult-webapp/src/controller/login.py b/ecs/jskult-webapp/src/controller/login.py index 5c8d904e..9b2c808e 100644 --- a/ecs/jskult-webapp/src/controller/login.py +++ b/ecs/jskult-webapp/src/controller/login.py @@ -70,11 +70,22 @@ def login( jwt_token = login_service.login(request.username, request.password) except NotAuthorizeException as e: logger.info(f'ログイン失敗:{e}') + # ログイン失敗回数をカウント + login_service.increase_login_failed_count(request.username) + # ログイン失敗回数を超過した場合はメッセージを変える + if login_service.is_login_failed_limit_exceeded(request.username): + login_service.on_login_fail_limit_exceeded(request.username) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_LOGIN_FAILED_LIMIT_EXCEEDED) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_LOGIN_ERROR) except JWTTokenVerifyException as e: logger.info(f'ログイン失敗:{e}') raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + # ログイン成功問わず、DBのログイン失敗回数が10回以上あれば、ログアウト画面にリダイレクトする + if login_service.is_login_failed_limit_exceeded(request.username): + logger.info(f'ログイン失敗回数が10回以上: {request.username}') + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=constants.LOGOUT_REASON_LOGIN_FAILED_LIMIT_EXCEEDED) + verified_token = jwt_token.verify_token() # 普通の認証だと、`cognito:username`に入る。 user_id = verified_token.user_id diff --git a/ecs/jskult-webapp/src/db/database.py b/ecs/jskult-webapp/src/db/database.py index c729faf5..bd828db3 100644 --- a/ecs/jskult-webapp/src/db/database.py +++ b/ecs/jskult-webapp/src/db/database.py @@ -189,7 +189,7 @@ class DatabaseClient: self.__session = None def to_jst(self): - self.execute('SET time_zone = "+9:00"') + self.execute('SET SESSION time_zone = "Asia/Tokyo"') def __execute_with_transaction(self, query: str, parameters: dict): # トランザクションを開始してクエリを実行する diff --git a/ecs/jskult-webapp/src/model/db/user_master.py b/ecs/jskult-webapp/src/model/db/user_master.py index 913b415e..d86253ae 100644 --- a/ecs/jskult-webapp/src/model/db/user_master.py +++ b/ecs/jskult-webapp/src/model/db/user_master.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Optional from src.model.db.base_db_model import BaseDBModel - +from src.system_var import constants class UserMasterModel(BaseDBModel): user_id: Optional[str] @@ -25,6 +25,8 @@ class UserMasterModel(BaseDBModel): updater: Optional[str] update_date: Optional[datetime] mntuser_flg: Optional[str] + mntuser_login_failed_cnt: Optional[int] + mntuser_last_login_failed_datetime: Optional[datetime] def is_enable_user(self): return self.enabled_flg == 'Y' @@ -34,3 +36,6 @@ class UserMasterModel(BaseDBModel): def is_groupware_user(self): return self.mntuser_flg == '0' or self.mntuser_flg is None + + def is_login_failed_limit_exceeded(self): + return self.mntuser_login_failed_cnt >= constants.LOGIN_FAIL_LIMIT \ No newline at end of file diff --git a/ecs/jskult-webapp/src/repositories/user_master_repository.py b/ecs/jskult-webapp/src/repositories/user_master_repository.py index 7edde00a..3acbc105 100644 --- a/ecs/jskult-webapp/src/repositories/user_master_repository.py +++ b/ecs/jskult-webapp/src/repositories/user_master_repository.py @@ -6,6 +6,19 @@ logger = get_logger('ユーザー取得') class UserMasterRepository(BaseRepository): + + def to_jst(self): + self._database.to_jst() + + def begin(self): + self._database.begin() + + def commit(self): + self._database.commit() + + def rollback(self): + self._database.rollback() + FETCH_SQL = """\ SELECT * @@ -26,3 +39,46 @@ class UserMasterRepository(BaseRepository): except Exception as e: logger.exception(f"DB Error : Exception={e}") raise e + + def increase_login_failed_count(self, parameter: dict) -> UserMasterModel: + try: + query = """\ + UPDATE + src05.user_mst + SET + mntuser_login_failed_cnt = + CASE + WHEN + DATE(mntuser_last_login_failed_datetime) = DATE(CURRENT_TIMESTAMP()) + THEN + mntuser_login_failed_cnt + 1 + ELSE + 1 + END, + mntuser_last_login_failed_datetime = CURRENT_TIMESTAMP() + WHERE + user_id = :user_id + AND + mntuser_flg = 1;\ + """ + self._database.execute(query, parameter) + except Exception as e: + logger.exception(f"DB Error : Exception={e}") + raise e + + def disable_mnt_user(self, parameter: dict) -> UserMasterModel: + try: + query = """\ + UPDATE + src05.user_mst + SET + enabled_flg = 'N' + WHERE + user_id = :user_id + AND + mntuser_flg = 1\ + """ + self._database.execute(query, parameter) + except Exception as e: + logger.exception(f"DB Error : Exception={e}") + raise e diff --git a/ecs/jskult-webapp/src/services/login_service.py b/ecs/jskult-webapp/src/services/login_service.py index aa1f37fd..3a2715b2 100644 --- a/ecs/jskult-webapp/src/services/login_service.py +++ b/ecs/jskult-webapp/src/services/login_service.py @@ -49,6 +49,27 @@ class LoginService(BaseService): user_record: UserMasterModel = self.user_repository.fetch_one({'user_id': user_id}) return user_record + def increase_login_failed_count(self, user_id: str): + + try: + # セッション内のタイムゾーン変更のため、明示的にトランザクションを開始する + self.user_repository.begin() + self.user_repository.to_jst() + self.user_repository.increase_login_failed_count({'user_id': user_id}) + self.user_repository.commit() + except Exception as e: + self.user_repository.rollback() + raise e + + def on_login_fail_limit_exceeded(self, user_id: str): + self.user_repository.disable_mnt_user({'user_id': user_id}) + + def is_login_failed_limit_exceeded(self, user_id: str): + user_record: UserMasterModel = self.user_repository.fetch_one({'user_id': user_id}) + if user_record is None: + return False + return user_record.is_login_failed_limit_exceeded() + def __secret_hash(self, username: str): # see - https://aws.amazon.com/jp/premiumsupport/knowledge-center/cognito-unable-to-verify-secret-hash/ # noqa message = bytes(username + environment.COGNITO_CLIENT_ID, 'utf-8') diff --git a/ecs/jskult-webapp/src/system_var/constants.py b/ecs/jskult-webapp/src/system_var/constants.py index 3756facb..962ed7d6 100644 --- a/ecs/jskult-webapp/src/system_var/constants.py +++ b/ecs/jskult-webapp/src/system_var/constants.py @@ -63,6 +63,7 @@ LOGOUT_REASON_BACKUP_PROCESSING = 'dump_processing' LOGOUT_REASON_NOT_LOGIN = 'not_login' LOGOUT_REASON_DB_ERROR = 'db_error' LOGOUT_REASON_UNEXPECTED = 'unexpected' +LOGOUT_REASON_LOGIN_FAILED_LIMIT_EXCEEDED = 'login_failed_limit_exceeded' LOGOUT_REASON_MESSAGE_MAP = { LOGOUT_REASON_DO_LOGOUT: 'Logoutしました。', @@ -72,7 +73,8 @@ LOGOUT_REASON_MESSAGE_MAP = { LOGOUT_REASON_BACKUP_PROCESSING: 'バックアップ取得を開始しました。
日次バッチ更新が終了するまでマスターメンテは使用できません', LOGOUT_REASON_NOT_LOGIN: 'Loginしてからページにアクセスしてください。', LOGOUT_REASON_DB_ERROR: 'DB接続に失敗しました。
再度Loginするか、
管理者にお問い合わせください。', - LOGOUT_REASON_UNEXPECTED: '予期しないエラーが発生しました。
再度Loginするか、
管理者に問い合わせてください。' + LOGOUT_REASON_UNEXPECTED: '予期しないエラーが発生しました。
再度Loginするか、
管理者に問い合わせてください。', + LOGOUT_REASON_LOGIN_FAILED_LIMIT_EXCEEDED: 'ログイン失敗回数の上限を超えましたので
アカウントをロックしました。
管理者に連絡してください' } # 新規施設担当者登録CSV(マスターメンテ) @@ -207,3 +209,6 @@ DISPLAY_USER_STOP_DIV_SHORT = { '03': '特定項目停止', '04': '全DM停止' } + +# ログイン失敗回数上限(保守ユーザー) +LOGIN_FAIL_LIMIT = 10 \ No newline at end of file