makabe.t dc52ec2022 Merged PR 765: データ削除ツール作成+動作確認
## 概要
[Task3569: データ削除ツール作成+動作確認](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3569)

- ADB2Cからのユーザー削除が100件ごとにしか削除できていなかったので、修正しました。
  - 取得が100件まででそのユーザーに対して削除処理をしていたので100件までの削除になっていました。
  - 対応として、100件づつの削除をユーザーが全削除されるまで実行するようにしました。

## レビューポイント
- 対応方法として適切でしょうか?
- ループで制限を設けていますが、MAX値として適切でしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで順に実行できることを確認
- 実際の削除は別途develop環境で実施します。
2024-02-22 07:33:55 +00:00

219 lines
7.0 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}`
);
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,
},
],
});
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" };
}
}
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}`);
}
}
}