## 概要 [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環境で実施します。
219 lines
7.0 KiB
TypeScript
219 lines
7.0 KiB
TypeScript
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}`);
|
||
}
|
||
}
|
||
}
|