masaaki a47ebaa9df Merged PR 798: [4回目実行][フルデータ]develop環境での移行実施後の修正作業
## 概要
[Task3821: [4回目実行][フルデータ]develop環境での移行実施後の修正作業](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3821)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 移行ツールに対して以下の修正を実施しました。
  - アカウントとユーザ間でAuthorIDが重複する際、通番を付与して重複を避けるようにしました
  - AADB2Cのエラー発生時、リトライ処理を行うように対応しました

## レビューポイント
- 特にありません

## UIの変更
- 無し

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
2024-03-02 02:21:19 +00:00

235 lines
7.6 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 { ClientSecretCredential } from "@azure/identity";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AdB2cResponse, AdB2cUser } from "./types/types";
import { isPromiseRejectedResult } from "./utils/utils";
import { Context } from "../../common/log";
import { ADB2C_SIGN_IN_TYPE } from "../../constants";
export type ConflictError = {
reason: "email";
message: string;
};
export class Adb2cTooManyRequestsError extends Error {}
export const isConflictError = (arg: unknown): arg is ConflictError => {
const value = arg as ConflictError;
if (value.message === undefined) {
return false;
}
if (value.reason === "email") {
return true;
}
return false;
};
@Injectable()
export class AdB2cService {
private readonly logger = new Logger(AdB2cService.name);
private readonly tenantName: string;
private graphClient: Client;
constructor(private readonly configService: ConfigService) {
this.tenantName = this.configService.getOrThrow<string>("TENANT_NAME");
// ADB2Cへの認証情報
const credential = new ClientSecretCredential(
this.configService.getOrThrow<string>("ADB2C_TENANT_ID"),
this.configService.getOrThrow<string>("ADB2C_CLIENT_ID"),
this.configService.getOrThrow<string>("ADB2C_CLIENT_SECRET")
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ["https://graph.microsoft.com/.default"],
});
this.graphClient = Client.initWithMiddleware({ authProvider });
}
/**
* Creates user AzureADB2Cにユーザーを追加する
* @param email 管理ユーザーのメールアドレス
* @param password 管理ユーザーのパスワード
* @param username 管理ユーザーの名前
* @returns user
*/
async createUser(
context: Context,
email: string,
password: string,
username: string
): Promise<{ sub: string } | ConflictError> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.createUser.name}`
);
const retryCount: number = 3;
let retry = 0;
while (retry < retryCount) {
try {
// ユーザをADB2Cに登録
const newUser = await this.graphClient.api("users/").post({
accountEnabled: true,
displayName: username,
passwordPolicies: "DisableStrongPassword",
passwordProfile: {
forceChangePasswordNextSignIn: false,
password: password,
},
identities: [
{
signinType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: `${this.tenantName}.onmicrosoft.com`,
issuerAssignedId: email,
},
],
});
this.logger.log(
`[${context.getTrackingId()}] [ADB2C CREATE] newUser: ${newUser}`
);
return { sub: newUser.id };
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e?.statusCode === 400 && e?.body) {
const error = JSON.parse(e.body);
// エラーが競合エラーである場合は、メールアドレス重複としてエラーを返す
if (error?.details?.find((x) => x.code === "ObjectConflict")) {
return { reason: "email", message: "ObjectConflict" };
}
}
if (++retry < retryCount) {
this.logger.log(`ADB2Cエラー発生。5秒sleepしてリトライします (${retry}/${retryCount})...`);
await new Promise(resolve => setTimeout(resolve, 5000));
} else {
this.logger.log(`リトライ数が上限に達したのでエラーを返却します`);
throw e;
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.createUser.name}`
);
}
}
}
/**
* Gets users
* @param externalIds
* @returns users
*/
async getUsers(
context: Context
): Promise<{ users: AdB2cUser[]; hasNext: boolean }> {
this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`);
try {
const res: AdB2cResponse = await this.graphClient
.api(`users/`)
.select(["id", "displayName", "identities"])
.filter(`creationType eq 'LocalAccount'`)
.get();
return { users: res.value, hasNext: !!res["@odata.nextLink"] };
} catch (e) {
this.logger.error(`error=${e}`);
const { statusCode } = e;
if (statusCode === 429) {
throw new Adb2cTooManyRequestsError();
}
throw e;
} finally {
this.logger.log(`[OUT] ${this.getUsers.name}`);
}
}
/**
* Azure AD B2Cからユーザ情報を削除する
* @param externalId 外部ユーザーID
* @param context コンテキスト
*/
async deleteUser(externalId: string, context: Context): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteUser.name
} | params: { externalId: ${externalId} };`
);
try {
// https://learn.microsoft.com/en-us/graph/api/user-delete?view=graph-rest-1.0&tabs=javascript#example
await this.graphClient.api(`users/${externalId}`).delete();
this.logger.log(
`[${context.getTrackingId()}] [ADB2C DELETE] externalId: ${externalId}`
);
// キャッシュからも削除する
// 移行ツール特別対応:キャッシュ登録は行わないので削除も不要
/*
try {
await this.redisService.del(context, makeADB2CKey(externalId));
} catch (e) {
// キャッシュからの削除に失敗しても、ADB2Cからの削除は成功しているため例外はスローしない
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
}
*/
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw e;
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`
);
}
}
/**
* Azure AD B2Cからユーザ情報を削除する複数
* @param externalIds 外部ユーザーID
*/
async deleteUsers(context: Context, externalIds: string[]): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteUsers.name
} | params: { externalIds: ${externalIds} };`
);
try {
// 複数ユーザーを一括削除する方法がないため、1人ずつで削除を行う
const results = await Promise.allSettled(
externalIds.map(async (externalId) => {
await this.graphClient.api(`users/${externalId}`).delete();
await new Promise((resolve) => setTimeout(resolve, 15)); // 15ms待つ
this.logger.log(`[[ADB2C DELETE] externalId: ${externalId}`);
})
);
// 失敗したプロミスのエラーをログに記録
results.forEach((result, index) => {
// statusがrejectedでない場合は、エラーが発生していないためログに記録しない
if (result.status !== "rejected") {
return;
}
const failedId = externalIds[index];
if (isPromiseRejectedResult(result)) {
const error = result.reason.toString();
this.logger.error(`Failed to delete user ${failedId}: ${error}`);
} else {
this.logger.error(`Failed to delete user ${failedId}`);
}
});
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.deleteUsers.name}`);
}
}
}