Merge pull request #355 release-202403 into master

This commit is contained in:
下田雅人 2024-03-08 15:47:04 +09:00
commit 408330ba77
14 changed files with 183 additions and 146 deletions

View File

@ -84,6 +84,8 @@
│   ├── exception_handler.py -- FastAPI内部でエラー発生時のハンドリング │   ├── exception_handler.py -- FastAPI内部でエラー発生時のハンドリング
│   └── exceptions.py -- カスタム例外クラス │   └── exceptions.py -- カスタム例外クラス
├── main.py -- APサーバーのエントリーポイント。ここでルーターやハンドラーの登録を行う ├── main.py -- APサーバーのエントリーポイント。ここでルーターやハンドラーの登録を行う
├── middleware -- ミドルウェアの設定
│ └── middleware.py
├── model -- モデル層(MVCのM) ├── model -- モデル層(MVCのM)
│   ├── db -- リポジトリから返されるDBレコードのモデル │   ├── db -- リポジトリから返されるDBレコードのモデル
│   │   ├── base_db_model.py │   │   ├── base_db_model.py
@ -195,3 +197,39 @@
- コントローラーのrouter変数が、`router.route_class = Authenticate`となっている場合、以下の動きをする - コントローラーのrouter変数が、`router.route_class = Authenticate`となっている場合、以下の動きをする
- リクエスト到達時にセッションの有無をチェックする - リクエスト到達時にセッションの有無をチェックする
- レスポンス時、クッキーにセッションキーを登録する - レスポンス時、クッキーにセッションキーを登録する
## HTMLで読み込んでいるスクリプトのSRIハッシュ値を生成・設定する方法
### サブリソース完全性 (Subresource Integrity, SRI) とは
CDN などから取得したリソースが意図せず改ざんされていないかをブラウザーが検証するセキュリティ機能です。 SRI を利用する際には、取得したリソースのハッシュ値と一致すべきハッシュ値を指定します。
詳細:<https://developer.mozilla.org/ja/docs/Web/Security/Subresource_Integrity>
実消化&アルトマークのWebアプリケーションでは、複数の外部スクリプトを読み込んで動作しているため、読み込むスクリプトを変更した場合は、
タグの属性値`integrity`に設定されているスクリプトのハッシュ値を更新する必要がある。
### SRI ハッシュ値の生成方法(サーバー内のスクリプトについて)
- サーバー内に保管されているスクリプトを更新した場合、Linux環境WSL2でも可で、以下のコマンドを実行し、ハッシュ値を生成する
```bash
cat <更新したスクリプトファイル名> | openssl dgst -sha384 -binary | openssl base64 -A
```
参考:<https://developer.mozilla.org/ja/docs/Web/Security/Subresource_Integrity#sri_%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B%E3%83%84%E3%83%BC%E3%83%AB>
### SRI ハッシュ値の生成方法(外部サイトから読み込んでいるスクリプトについて)
- 外部サイトから読み込んでいるスクリプトを更新した場合、下記のMDNオンラインツールでハッシュ値を生成する
- [SRI Hash Generator](https://www.srihash.org/)
### SRI ハッシュ値の設定方法
- 更新したスクリプトを読み込んでいる箇所の`integrity`属性値を、生成したハッシュ値に置き換える
- 以下は設定のサンプル
```bash
<script src="https://リンク/スクリプト.js" integrity="sha384-生成したハッシュ" crossorigin="anonymous"></script>
```

View File

@ -79,11 +79,11 @@ def search_bio_data(
'data': data, 'data': data,
'count': bio_sales_lot_count 'count': bio_sales_lot_count
}) })
# クッキーも書き換え # クッキーも書き換え
json_response.set_cookie( json_response.set_cookie(
key='session', key='session',
value=session.session_key, value=session.session_key,
max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける
secure=True, secure=True,
httponly=True httponly=True
) )
@ -153,10 +153,10 @@ async def download_bio_data(
'status': 'ok', 'status': 'ok',
'download_url': download_file_url 'download_url': download_file_url
}) })
json_response.set_cookie( json_response.set_cookie(
key='session', key='session',
value=session.session_key, value=session.session_key,
max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける
secure=True, secure=True,
httponly=True httponly=True
) )

View File

@ -113,6 +113,7 @@ def login(
status_code=status.HTTP_303_SEE_OTHER, status_code=status.HTTP_303_SEE_OTHER,
headers={'session_key': session_key} headers={'session_key': session_key}
) )
return response return response
@ -170,4 +171,5 @@ def sso_authorize(
status_code=status.HTTP_303_SEE_OTHER, status_code=status.HTTP_303_SEE_OTHER,
headers={'session_key': session_key} headers={'session_key': session_key}
) )
return response return response

View File

@ -8,6 +8,7 @@ from src.model.internal.session import UserSession
from src.model.view.logout_view_model import LogoutViewModel from src.model.view.logout_view_model import LogoutViewModel
from src.system_var import constants from src.system_var import constants
from src.templates import templates from src.templates import templates
from src.services import session_service
router = APIRouter() router = APIRouter()
@ -16,6 +17,7 @@ router = APIRouter()
######################### #########################
@router.get('/', response_class=HTMLResponse) @router.get('/', response_class=HTMLResponse)
def logout_view( def logout_view(
request: Request, request: Request,
@ -47,4 +49,9 @@ def logout_view(
) )
# クッキーを削除 # クッキーを削除
template_response.delete_cookie('session') template_response.delete_cookie('session')
# セッション削除
if session:
session_service.delete_session(session)
return template_response return template_response

View File

@ -10,8 +10,9 @@ from src.controller import (bio, bio_api, healthcheck, login, logout,
from src.core import task from src.core import task
from src.error.exception_handler import http_exception_handler from src.error.exception_handler import http_exception_handler
from src.error.exceptions import UnexpectedException from src.error.exceptions import UnexpectedException
from src.middleware.middleware import SecurityHeadersMiddleware
app = FastAPI() app = FastAPI(openapi_url=None)
# 静的ファイルをマウント # 静的ファイルをマウント
app.mount('/static', StaticFiles(directory=path.dirname(static.__file__)), name='static') app.mount('/static', StaticFiles(directory=path.dirname(static.__file__)), name='static')
@ -42,5 +43,8 @@ app.add_exception_handler(status.HTTP_403_FORBIDDEN, http_exception_handler)
# サーバーエラーが発生した場合のハンドラー。HTTPExceptionではハンドリングできないため、個別に設定 # サーバーエラーが発生した場合のハンドラー。HTTPExceptionではハンドリングできないため、個別に設定
app.add_exception_handler(UnexpectedException, http_exception_handler) app.add_exception_handler(UnexpectedException, http_exception_handler)
# セキュリティヘッダー設定はミドルウェアで処理する
app.add_middleware(SecurityHeadersMiddleware)
# サーバー起動時のイベント # サーバー起動時のイベント
app.add_event_handler('startup', task.create_start_app_handler()) app.add_event_handler('startup', task.create_start_app_handler())

View File

@ -0,0 +1,16 @@
from fastapi import Request, Response, status
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
# X-Frame-Optionsヘッダー追加
response.headers['X-Frame-Options'] = 'DENY'
# X-Content-Type-Optionsヘッダー追加
response.headers['X-Content-Type-Options'] = 'nosniff'
# Strict-Transport-Securityヘッダー追加
response.headers['Strict-Transport-Security'] = 'max-age=31536000 includeSubDomains'
# Cache-Controlヘッダー追加
response.headers['Cache-Control'] = 'private'
return response

View File

@ -103,6 +103,7 @@ class AfterSetCookieSessionRoute(MeDaCaRoute):
"""事後処理として、セッションキーをcookieに設定するカスタムルートハンドラー""" """事後処理として、セッションキーをcookieに設定するカスタムルートハンドラー"""
async def post_process_route(self, request: Request, response: Response): async def post_process_route(self, request: Request, response: Response):
response = await super().post_process_route(request, response) response = await super().post_process_route(request, response)
session_key = response.headers.get('session_key', None) session_key = response.headers.get('session_key', None)
# セッションキーがない場合はセットせずに返す # セッションキーがない場合はセットせずに返す
if session_key is None: if session_key is None:
@ -123,7 +124,6 @@ class AfterSetCookieSessionRoute(MeDaCaRoute):
response.set_cookie( response.set_cookie(
key='session', key='session',
value=session_key, value=session_key,
max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける
secure=True, secure=True,
httponly=True httponly=True
) )

View File

@ -17,3 +17,10 @@ def get_session(key: str) -> UserSession:
except UserSession.DoesNotExist as e: except UserSession.DoesNotExist as e:
logger.debug(f'セッション取得失敗:{e}') logger.debug(f'セッション取得失敗:{e}')
return None return None
def delete_session (session: UserSession):
try:
session.delete()
return
except:
return

View File

@ -1,19 +1,19 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, address=no" http-equiv="content-type" content="text/html; charset=utf-8" /> <meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="format-detection" content="telephone=no, address=no" />
<title>{{subtitle}}</title> <title>{{subtitle}}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css" integrity="sha384-b6lVK+yci+bfDmaY1u0zE8YYJt0TZxLEAFyYSLHId4xoVvsrQu3INevFKo+Xir8e" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"> <link rel="stylesheet" href="/static/css/main_theme.css" integrity="sha384-k0YpJBvcGJXdJlt8yqnhPYuU7tHQfdv4C80KDdGf72dzzAVWUp+ek+A1cqOV5o4t">
<link rel="stylesheet" href="/static/css/main_theme.css"> <link rel="stylesheet" href="/static/css/pagenation.css" integrity="sha384-CDhOHftwvzWdI3cmvl0PESIdU5i0qjWbz8+HE9poJscglyrB0jzXZpVkb51xigty">
<link rel="stylesheet" href="/static/css/pagenation.css"> <link rel="stylesheet" href="/static/css/datepicker.css" integrity="sha384-I3gPqeqj0wDLoF6oS/OuMJ5C+BI210zLrJvQvNRVdvyyI9+qrraaQK2L9vvhTA8x">
<link rel="stylesheet" href="/static/css/datepicker.css"> <link rel="stylesheet" href="/static/css/loading.css" integrity="sha384-f9FRohCbLarb6Z91FWRbfNIIYYLx/5Kxqw19CB9Z0GxXunS9j0gRWWl50LayDAG7">
<link rel="stylesheet" href="/static/css/loading.css"> <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/paginationjs@2.5.0/dist/pagination.min.js" crossorigin="anonymous"></script>
<script src="https://pagination.js.org/dist/2.5.0/pagination.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.min.js"></script> <script src="/static/function/businessLogicScript.js" integrity="sha384-ytd1o7Rx4BPzjO3RpzR9fW/Z4avGzS7+BRPZVUsQp5X4zXB6xdZpR47/En1mNl7s" crossorigin="anonymous"></script>
<script src="/static/function/businessLogicScript.js"></script> <script src="/static/lib/fixed_midashi.js" integrity="sha384-mCd6L3DNaLgUWyH051BywJfzlVavCkK6F0wbMqG+j7jAq174Uf7HJdq3H4wxCJKs" crossorigin="anonymous"></script>
<script src="/static/lib/fixed_midashi.js"></script>

View File

@ -15,61 +15,8 @@
{{logout.reason}} {{logout.reason}}
{% endautoescape %} {% endautoescape %}
</p> </p>
<!-- <?php
// getが来ておらず理由がわからない場合
if(!(isset($_GET['reason']))){
$userflg = null;
// ログアウトボタンを押されたとき
} else if($_GET['reason'] == 'logoutBtn'){
?>
<p class="logout_p"><?php echo $logoutMsg ?></p>
<?php
// ログイン失敗時に表示
} else if($_GET['reason'] == 'loginErr'){
?>
<p class="logout_p"><?php echo $loginErrMsg ?></p>
<?php
// 日時バッチ処理中エラー時
} else if($_GET['reason'] == 'batchProcess'){
?>
<p class="logout_p"><?php echo $batchProcessMsg ?></p>
<?php
// マスターメンテ日時バッチ処理中エラー時
} else if($_GET['reason'] == 'batchProcessNewInstEmpRegist'){
?>
<p class="logout_p"><?php echo $batchProcessNewInstEmpRegistMsg ?></p>
<?php
// どっちのユーザーでログインしたかわからないとき
} else if (!(isset($userflg))) {
} else {
$userflg = null;
?>
<p class="logout_p"><?php echo $unexpectedErrMsg ?></p>
<?php
}
?> -->
<br><br><br> <br><br><br>
<p class="logout_p"><a href="{{ logout.redirect_to }}">{{logout.link_text}}</a></p> <p class="logout_p"><a href="{{ logout.redirect_to }}">{{logout.link_text}}</a></p>
<!-- MeDaCA機能メニューへ -->
<!-- <p class="logout_p"><a href="redirect_to">Login画面に戻る</a></p> -->
<!-- <?php
if (!(isset($userflg))) {
?>
<p class="logout_p"><a href="<?php echo $groupwarePath ?>"><?php echo $groupwareBackMsg ?></a></p>
<?php
} else if($userflg == 1){
?>
<p class="logout_p"><a href="<?php echo $maintLoginPath ?>"><?php echo $loginBackMsg ?></a></p>
<?php
} else {
?>
<p class="logout_p"><a href="<?php echo $groupwarePath ?>"><?php echo $groupwareBackMsg ?></a></p>
<?php
}
?> -->
</div> </div>
</body> </body>
</html> </html>

View File

@ -2659,7 +2659,22 @@
"MSJ_Child_Account_Name__c", "MSJ_Child_Account_Name__c",
"MSJ_Child_Account__c", "MSJ_Child_Account__c",
"MSJ_Parent_Account_Name__c", "MSJ_Parent_Account_Name__c",
"MSJ_Parent_Child_Name__c" "MSJ_Parent_Child_Name__c",
"MSJ_Age__c",
"MSJ_Comments__c",
"MSJ_Effectiveness_evaluation_date__c",
"MSJ_Gender__c",
"MSJ_Informed_consent__c",
"MSJ_Metastases_location__c",
"MSJ_PS__c",
"MSJ_Scheduled_date_of_informed_consent__c",
"MSJ_Scheduled_start_date_administration__c",
"MSJ_CDX_type__c",
"MSJ_First_line_chemotherapy_efficacy__c",
"MSJ_Num_cycles_of_1st_line_chemotherapy__c",
"MSJ_Primary_site_location__c",
"MSJ_Regimen_of_first_line_chemotherapy__c",
"MSJ_Starting_dose__c"
], ],
"is_skip": false, "is_skip": false,
"is_update_last_fetch_datetime": true "is_update_last_fetch_datetime": true

View File

@ -4,10 +4,10 @@ utf-8
" "
CRLF CRLF
1 1
37 52
Id,OwnerId,IsDeleted,Name,CreatedDate,CreatedById,LastModifiedDate,LastModifiedById,SystemModstamp,MayEdit,IsLocked,MSJ_Account_Name__c,MSJ_CRC_Group__c,MSJ_Casus_or_Transfer_Point__c,MSJ_Entry_Date__c,MSJ_IST_Name__c,MSJ_Indication__c,MSJ_Line__c,MSJ_MR_Comments__c,MSJ_MUID__c,MSJ_Medical_Regimen__c,MSJ_Month__c,MSJ_Report_Comments__c,MSJ_Start_Date_Of_Administration__c,MSJ_Year__c,Mobile_ID_vod__c,MSJ_CRC_RAS_KRAS__c,MSJ_End_Date_Of_Administration__c,MSJ_End_Date_of_Stop_Administration__c,MSJ_HN_Hospitalized_Type__c,MSJ_Start_Date_of_Stop_Administration__c,MSJ_Patient_Status__c,MSJ_Patient_TA__c,MSJ_Child_Account_Name__c,MSJ_Child_Account__c,MSJ_Parent_Account_Name__c,MSJ_Parent_Child_Name__c Id,OwnerId,IsDeleted,Name,CreatedDate,CreatedById,LastModifiedDate,LastModifiedById,SystemModstamp,MayEdit,IsLocked,MSJ_Account_Name__c,MSJ_CRC_Group__c,MSJ_Casus_or_Transfer_Point__c,MSJ_Entry_Date__c,MSJ_IST_Name__c,MSJ_Indication__c,MSJ_Line__c,MSJ_MR_Comments__c,MSJ_MUID__c,MSJ_Medical_Regimen__c,MSJ_Month__c,MSJ_Report_Comments__c,MSJ_Start_Date_Of_Administration__c,MSJ_Year__c,Mobile_ID_vod__c,MSJ_CRC_RAS_KRAS__c,MSJ_End_Date_Of_Administration__c,MSJ_End_Date_of_Stop_Administration__c,MSJ_HN_Hospitalized_Type__c,MSJ_Start_Date_of_Stop_Administration__c,MSJ_Patient_Status__c,MSJ_Patient_TA__c,MSJ_Child_Account_Name__c,MSJ_Child_Account__c,MSJ_Parent_Account_Name__c,MSJ_Parent_Child_Name__c,MSJ_Age__c,MSJ_Comments__c,MSJ_Effectiveness_evaluation_date__c,MSJ_Gender__c,MSJ_Informed_consent__c,MSJ_Metastases_location__c,MSJ_PS__c,MSJ_Scheduled_date_of_informed_consent__c,MSJ_Scheduled_start_date_administration__c,MSJ_CDX_type__c,MSJ_First_line_chemotherapy_efficacy__c,MSJ_Num_cycles_of_1st_line_chemotherapy__c,MSJ_Primary_site_location__c,MSJ_Regimen_of_first_line_chemotherapy__c,MSJ_Starting_dose__c
Id,OwnerId,IsDeleted,Name,CreatedDate,CreatedById,LastModifiedDate,LastModifiedById,SystemModstamp,MayEdit,IsLocked,MSJ_Account_Name__c,MSJ_CRC_Group__c,MSJ_Casus_or_Transfer_Point__c,MSJ_Entry_Date__c,MSJ_IST_Name__c,MSJ_Indication__c,MSJ_Line__c,MSJ_MR_Comments__c,MSJ_MUID__c,MSJ_Medical_Regimen__c,MSJ_Month__c,MSJ_Report_Comments__c,MSJ_Start_Date_Of_Administration__c,MSJ_Year__c,Mobile_ID_vod__c,MSJ_CRC_RAS_KRAS__c,MSJ_End_Date_Of_Administration__c,MSJ_End_Date_of_Stop_Administration__c,MSJ_HN_Hospitalized_Type__c,MSJ_Start_Date_of_Stop_Administration__c,MSJ_Patient_Status__c,MSJ_Patient_TA__c,MSJ_Child_Account_Name__c,MSJ_Child_Account__c,MSJ_Parent_Account_Name__c,MSJ_Parent_Child_Name__c Id,OwnerId,IsDeleted,Name,CreatedDate,CreatedById,LastModifiedDate,LastModifiedById,SystemModstamp,MayEdit,IsLocked,MSJ_Account_Name__c,MSJ_CRC_Group__c,MSJ_Casus_or_Transfer_Point__c,MSJ_Entry_Date__c,MSJ_IST_Name__c,MSJ_Indication__c,MSJ_Line__c,MSJ_MR_Comments__c,MSJ_MUID__c,MSJ_Medical_Regimen__c,MSJ_Month__c,MSJ_Report_Comments__c,MSJ_Start_Date_Of_Administration__c,MSJ_Year__c,Mobile_ID_vod__c,MSJ_CRC_RAS_KRAS__c,MSJ_End_Date_Of_Administration__c,MSJ_End_Date_of_Stop_Administration__c,MSJ_HN_Hospitalized_Type__c,MSJ_Start_Date_of_Stop_Administration__c,MSJ_Patient_Status__c,MSJ_Patient_TA__c,MSJ_Child_Account_Name__c,MSJ_Child_Account__c,MSJ_Parent_Account_Name__c,MSJ_Parent_Child_Name__c,MSJ_Age__c,MSJ_Comments__c,MSJ_Effectiveness_evaluation_date__c,MSJ_Gender__c,MSJ_Informed_consent__c,MSJ_Metastases_location__c,MSJ_PS__c,MSJ_Scheduled_date_of_informed_consent__c,MSJ_Scheduled_start_date_administration__c,MSJ_CDX_type__c,MSJ_First_line_chemotherapy_efficacy__c,MSJ_Num_cycles_of_1st_line_chemotherapy__c,MSJ_Primary_site_location__c,MSJ_Regimen_of_first_line_chemotherapy__c,MSJ_Starting_dose__c
src02c.crm_MSJ_Patient__c src02c.crm_MSJ_Patient__c
org02.crm_MSJ_Patient__c org02.crm_MSJ_Patient__c
CRM_MSJ_Patient__c_ex.sql

View File

@ -0,0 +1 @@
CALL internal02.crm_history('src02c.crm_MSJ_Patient__c', 'SystemModstamp');