makabe.t 04c726e964 Merged PR 808: function修正
## 概要
[Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-03-07 11:47:53 +00:00

516 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { app, InvocationContext, Timer } from "@azure/functions";
import * as dotenv from "dotenv";
import { BlobstorageService } from "../blobstorage/blobstorage.service";
import {
ADMIN_ROLES,
IMPORT_USERS_MAX_DURATION_MINUTES,
IMPORT_USERS_STAGE_FILE_NAME,
IMPORT_USERS_STAGES,
RoleNumberMap,
SYSTEM_IMPORT_USERS,
TIERS,
} from "../constants";
import { ErrorRow, ImportData } from "../blobstorage/types/types";
import { Configuration, UsersApi } from "../api";
import { createErrorObject } from "../common/errors/utils";
import { sign, getJwtKey } from "../common/jwt";
import { AccessToken, SystemAccessToken } from "../common/jwt/types";
import { isImportJson, isStageJson } from "../blobstorage/types/guards";
import https from "https";
export async function importUsersProcessing(
context: InvocationContext,
blobstorageService: BlobstorageService,
userApi: UsersApi
): Promise<void> {
context.log(`[IN] importUsersProcessing`);
try {
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.local", override: true });
const startUnixTime = getCurrentUnixTime();
context.log(`importUsersProcessing start: ${startUnixTime}`);
// ファイルが存在する間ループ
while (true) {
// Blobストレージからファイル名の一覧を取得stage.json以外
const bloblist = await blobstorageService.listBlobs(context);
context.log(bloblist);
// stage.json以外のファイルが存在しない場合は処理中断
if (bloblist.length === 0) {
break;
}
// ファイルのうち、日付が最も古いファイルを取得
let targetFileName = bloblist.sort().at(0);
if (targetFileName === undefined) {
throw new Error("targetFileName is undefined");
}
let row = 1;
// stage.jsonを取得ダウンロードして読み込む
let stageData = await blobstorageService.downloadFileData(
context,
IMPORT_USERS_STAGE_FILE_NAME
);
// stage.jsonが存在しない場合は、新規作成する
if (stageData === undefined) {
stageData = JSON.stringify({
update: getCurrentUnixTime(),
state: IMPORT_USERS_STAGES.CREATED,
});
const updateSuccess = await blobstorageService.updateFile(
context,
IMPORT_USERS_STAGE_FILE_NAME,
stageData
);
if (!updateSuccess) {
throw new Error(
`update stage.json failed. state: ${IMPORT_USERS_STAGES.CREATED} filename: ${targetFileName}`
);
}
}
const stage = JSON.parse(stageData);
if (!isStageJson(stage)) {
throw new Error("stage.json is invalid");
}
// 作業中のstage.jsonが存在する場合は、処理を再開する
if (
stage.state !== IMPORT_USERS_STAGES.CREATED &&
stage.state !== IMPORT_USERS_STAGES.DONE
) {
// stage.jsonが存在し、内部状態が処理中で、最終更新日時が10分以上前だった場合は処理中断とみなして途中から再開
const nowUnixTime = getCurrentUnixTime();
if (nowUnixTime - stage.update > 10 * 60) {
// stage.jsonの内容から処理対象のfilepathを特定する
context.log(stage.filename);
if (stage.filename === undefined) {
context.log("stage.filename is undefined");
break;
}
targetFileName = stage.filename;
// 処理開始行をstage.jsonを元に復元する
row = stage.row ?? 1;
} else {
// 内部状態が処理中であれば処理中断処理が終わる前にTimerから再度起動されてしまったケース
context.log("stage is processing");
break;
}
}
{
const updateSuccess = await blobstorageService.updateFile(
context,
IMPORT_USERS_STAGE_FILE_NAME,
JSON.stringify({
update: getCurrentUnixTime(),
state: IMPORT_USERS_STAGES.PRAPARE,
filename: targetFileName,
})
);
if (!updateSuccess) {
throw new Error(
`update stage.json failed. state: ${IMPORT_USERS_STAGES.PRAPARE} filename: ${targetFileName}`
);
}
}
// 対象ファイルをダウンロードして読み込む
const importsData = await blobstorageService.downloadFileData(
context,
targetFileName
);
// 一括登録ユーザー一覧をメモリ上に展開
const imports =
importsData === undefined ? undefined : JSON.parse(importsData);
if (!isImportJson(context, imports)) {
throw new Error(`json: ${targetFileName} is invalid`);
}
if (imports === undefined) {
break;
}
// 代行操作トークンを発行する
const accsessToken = await generateDelegationAccessToken(
context,
imports.external_id,
imports.user_role
);
// 一括登録ユーザー一覧をループして、一括登録ユーザーを一括登録する
const errors: ErrorRow[] = [];
for (const user of imports.data) {
{
// stage.jsonを更新ユーザー追加開始
const updateSuccess = await blobstorageService.updateFile(
context,
IMPORT_USERS_STAGE_FILE_NAME,
JSON.stringify({
update: getCurrentUnixTime(),
state: IMPORT_USERS_STAGES.START,
filename: targetFileName,
row: row,
})
);
if (!updateSuccess) {
throw new Error(
`update stage.json failed. state: ${IMPORT_USERS_STAGES.START} filename: ${targetFileName} row: ${row}`
);
}
}
try {
if (!checkUser(context, user, targetFileName, row)) {
throw new Error(
`Invalid user data. filename: ${targetFileName} row: ${row}`
);
}
// ユーザーを追加する
await addUser(context, userApi, user, accsessToken);
} catch (e) {
const error = createErrorObject(e);
context.log(error);
// エラーが発生したらエラーコードを控えておく
errors.push({ row: row, error: error.code, name: user.name });
}
{
// stage.jsonを更新ユーザー追加完了
const updateSuccess = await blobstorageService.updateFile(
context,
IMPORT_USERS_STAGE_FILE_NAME,
JSON.stringify({
update: getCurrentUnixTime(),
state: IMPORT_USERS_STAGES.COMPLETE,
filename: targetFileName,
row: row,
errors: errors,
})
);
if (!updateSuccess) {
throw new Error(
`update stage.json failed. state: ${IMPORT_USERS_STAGES.COMPLETE} filename: ${targetFileName} row: ${row}`
);
}
}
row++;
// 500ms待機
await new Promise((resolve) => setTimeout(resolve, 500));
}
// 処理対象のユーザー一覧ファイルを削除する
await blobstorageService.deleteFile(context, targetFileName);
// システムトークンを発行
const systemToken = await generateSystemToken(context);
// 一括登録完了メールを送信するODMS Cloudの一括追加完了APIを呼び出す
await userApi.multipleImportsComplate(
{
accountId: imports.account_id,
filename: imports.file_name,
requestTime: getCurrentUnixTime(),
errors: errors.map((error) => {
return {
name: error.name,
line: error.row,
errorCode: error.error,
};
}),
},
{
headers: { authorization: `Bearer ${systemToken}` },
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
}
);
{
// stage.jsonを更新処理完了
const updateSuccess = await blobstorageService.updateFile(
context,
IMPORT_USERS_STAGE_FILE_NAME,
JSON.stringify({
update: getCurrentUnixTime(),
state: IMPORT_USERS_STAGES.DONE,
})
);
if (!updateSuccess) {
throw new Error(
`update stage.json failed. state: ${IMPORT_USERS_STAGES.DONE} filename: ${targetFileName}`
);
}
}
// 経過時間を確認して、30分以上経過していたら処理を中断する
{
const currentUnixTime = getCurrentUnixTime();
// 時間の差分を計算(秒)
const elapsedSec = currentUnixTime - startUnixTime;
// 30分以上経過していたら処理を中断する
if (elapsedSec > IMPORT_USERS_MAX_DURATION_MINUTES * 60) {
context.log("timeout");
break;
}
}
}
} catch (e) {
context.log("importUsers failed.");
context.error(e);
throw e;
} finally {
context.log(`[OUT] importUsersProcessing`);
}
}
export async function importUsers(
myTimer: Timer,
context: InvocationContext
): Promise<void> {
context.log(`[IN] importUsers`);
try {
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.local", override: true });
const blobstorageService = new BlobstorageService();
const userApi = new UsersApi(
new Configuration({
basePath: process.env.BASE_PATH,
})
);
await importUsersProcessing(context, blobstorageService, userApi);
} catch (e) {
context.log("importUsers failed.");
context.error(e);
throw e;
} finally {
context.log(`[OUT] importUsers`);
}
}
/**
* ODMS CloudのAPIを呼び出してユーザーを追加する
* @param context
* @param user
* @returns user
*/
export async function addUser(
context: InvocationContext,
userApi: UsersApi,
user: ImportData,
token: string
): Promise<void> {
context.log(`[IN] addUser`);
try {
await userApi.signup(
{
email: user.email,
name: user.name,
role: RoleNumberMap[user.role],
autoRenew: user.auto_renew === 1,
notification: user.notification === 1,
authorId: user.role === 1 ? user.author_id : undefined,
encryption: user.role === 1 ? user.encryption === 1 : undefined,
encryptionPassword:
user.encryption === 1 ? user.encryption_password : undefined,
prompt: user.role === 1 ? user.prompt === 1 : undefined,
},
{
headers: { authorization: `Bearer ${token}` },
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
}
);
} catch (e) {
context.error(e);
context.error(JSON.stringify(e.response?.data));
throw e;
} finally {
context.log(`[OUT] addUser`);
}
}
/**
* ユーザーのデータが正しいかどうかをチェック
* @param context
* @param user
* @param fileName
* @param row
* @returns true if user
*/
function checkUser(
context: InvocationContext,
user: ImportData,
fileName: string,
row: number
): boolean {
context.log(
`[IN] checkUser | params: { fileName: ${fileName}, row: ${row} }`
);
try {
// 名前が255文字以内であること
if (user.name.length > 255) {
context.log(`name is too long. fileName: ${fileName}, row: ${row}`);
return false;
}
const emailPattern =
/^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/;
// メールアドレスが255文字以内であること
if (user.email.length > 255) {
context.log(`email is too long. fileName: ${fileName}, row: ${row}`);
return false;
}
if (!emailPattern.test(user.email)) {
context.log(`Invalid email. fileName: ${fileName}, row: ${row}`);
return false;
}
// ロールが(0/1/2)のいずれかであること
if (![0, 1, 2].includes(user.role)) {
context.log(`Invalid role number. fileName: ${fileName}, row: ${row}`);
return false;
}
// ロールがAuthorの場合
if (user.role === 1) {
// author_idが必須
if (user.author_id === undefined) {
context.log(
`author_id is required. fileName: ${fileName}, row: ${row}`
);
return false;
}
// author_idが16文字以内であること
if (user.author_id.length > 16) {
context.log(
`author_id is too long. fileName: ${fileName}, row: ${row}`
);
return false;
}
// author_idが半角大文字英数字とハイフンであること
if (!/^[A-Z0-9_]*$/.test(user.author_id)) {
context.log(`author_id is invalid. fileName: ${fileName}, row: ${row}`);
return false;
}
// encryptionが必須
if (user.encryption === undefined) {
context.log(
`encryption is required. fileName: ${fileName}, row: ${row}`
);
return false;
}
// encryptionが1の場合
if (user.encryption === 1) {
// encryption_passwordが必須
if (user.encryption_password === undefined) {
context.log(
`encryption_password is required. fileName: ${fileName}, row: ${row}`
);
return false;
}
// 416文字の半角英数字と記号のみであること
if (!/^[!-~]{4,16}$/.test(user.encryption_password)) {
context.log(
`encryption_password is invalid. fileName: ${fileName}, row: ${row}`
);
return false;
}
if (user.prompt === undefined) {
context.log(`prompt is required. fileName: ${fileName}, row: ${row}`);
return false;
}
}
}
return true;
} catch (e) {
context.error(e);
throw e;
} finally {
context.log(`[OUT] checkUser`);
}
}
/**
* 代行操作用のアクセストークンを生成します
* @param context
* @param externalId
* @returns delegation token
*/
async function generateDelegationAccessToken(
context: InvocationContext,
externalId: string,
role: string
): Promise<string> {
context.log(
`[IN] generateDelegationAccessToken | params: { externalId: ${externalId} }`
);
try {
// 要求されたトークンの寿命を決定
const tokenLifetime = Number(process.env.ACCESS_TOKEN_LIFETIME_WEB);
const privateKey = getJwtKey(process.env.JWT_PRIVATE_KEY ?? "");
const token = sign<AccessToken>(
{
role: `${role} ${ADMIN_ROLES.ADMIN}`,
tier: TIERS.TIER5,
userId: externalId,
delegateUserId: SYSTEM_IMPORT_USERS,
},
tokenLifetime,
privateKey
);
return token;
} catch (e) {
context.error(e);
throw e;
} finally {
context.log(`[OUT] generateDelegationAccessToken`);
}
}
/**
* System用のアクセストークンを生成します
* @param context
* @returns system token
*/
async function generateSystemToken(
context: InvocationContext
): Promise<string> {
context.log(`[IN] generateSystemToken`);
try {
// 要求されたトークンの寿命を決定
const tokenLifetime = Number(process.env.ACCESS_TOKEN_LIFETIME_WEB);
const privateKey = getJwtKey(process.env.JWT_PRIVATE_KEY ?? "");
const token = sign<SystemAccessToken>(
{
systemName: SYSTEM_IMPORT_USERS,
},
tokenLifetime,
privateKey
);
return token;
} catch (e) {
context.error(e);
throw e;
} finally {
context.log(`[OUT] generateSystemToken`);
}
}
const getCurrentUnixTime = () => Math.floor(new Date().getTime() / 1000);
// 5分毎に実行
app.timer("importUsers", {
schedule: "0 */5 * * * *",
handler: importUsers,
});