Merged PR 583: [ライセンスアラート改善]AzureAdB2Cアクセスの効率化
## 概要 [Task3023: [ライセンスアラート改善]AzureAdB2Cアクセスの効率化](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3023) ADB2Cからユーザーを取得する際に、Redisによるキャッシュ保存・キャッシュからの取得を行う処理を実装しました。 ## レビューポイント 処理の妥当性などを全体的にお願いします。 ## UIの変更 なし ## 動作確認状況 ローカルで動作確認済み ## 補足 なし
This commit is contained in:
parent
15fa10e265
commit
fee99a0974
29
dictation_function/package-lock.json
generated
29
dictation_function/package-lock.json
generated
@ -13,11 +13,13 @@
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"mysql2": "^2.3.3",
|
||||
"redis": "^3.1.2",
|
||||
"typeorm": "^0.3.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/node": "18.x",
|
||||
"@types/redis": "^2.8.13",
|
||||
"azure-functions-core-tools": "^4.x",
|
||||
"jest": "^28.0.3",
|
||||
"rimraf": "^5.0.0",
|
||||
@ -1989,6 +1991,15 @@
|
||||
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/redis": {
|
||||
"version": "2.8.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
|
||||
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz",
|
||||
@ -5252,9 +5263,9 @@
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
|
||||
"integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
|
||||
"integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "14 || >=16.14"
|
||||
@ -6371,8 +6382,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
|
||||
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"denque": "^1.5.0",
|
||||
"redis-commands": "^1.7.0",
|
||||
@ -6390,16 +6399,12 @@
|
||||
"node_modules/redis-commands": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
|
||||
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@ -6408,8 +6413,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"redis-errors": "^1.0.0"
|
||||
},
|
||||
@ -6421,8 +6424,6 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
|
||||
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
|
||||
@ -18,11 +18,13 @@
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"mysql2": "^2.3.3",
|
||||
"redis": "^3.1.2",
|
||||
"typeorm": "^0.3.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/node": "18.x",
|
||||
"@types/redis": "^2.8.13",
|
||||
"azure-functions-core-tools": "^4.x",
|
||||
"jest": "^28.0.3",
|
||||
"rimraf": "^5.0.0",
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
import { ClientSecretCredential } from "@azure/identity";
|
||||
import { Client } from "@microsoft/microsoft-graph-client";
|
||||
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
|
||||
import { AdB2cResponse, AdB2cUser } from "./types/types";
|
||||
import { error } from "console";
|
||||
|
||||
export class Adb2cTooManyRequestsError extends Error {}
|
||||
|
||||
export class AdB2cService {
|
||||
private graphClient: Client;
|
||||
|
||||
constructor() {
|
||||
// ADB2Cへの認証情報
|
||||
if (
|
||||
!process.env.ADB2C_TENANT_ID ||
|
||||
!process.env.ADB2C_CLIENT_ID ||
|
||||
!process.env.ADB2C_CLIENT_SECRET
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
const credential = new ClientSecretCredential(
|
||||
process.env.ADB2C_TENANT_ID,
|
||||
process.env.ADB2C_CLIENT_ID,
|
||||
process.env.ADB2C_CLIENT_SECRET
|
||||
);
|
||||
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
|
||||
scopes: ["https://graph.microsoft.com/.default"],
|
||||
});
|
||||
|
||||
this.graphClient = Client.initWithMiddleware({ authProvider });
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure AD B2Cからユーザ情報を取得する
|
||||
* @param externalIds 外部ユーザーID
|
||||
* @returns ユーザ情報
|
||||
*/
|
||||
async getUsers(externalIds: string[]): Promise<AdB2cUser[]> {
|
||||
const chunkExternalIds = splitArrayInChunksOfFifteen(externalIds);
|
||||
|
||||
try {
|
||||
const b2cUsers: AdB2cUser[] = [];
|
||||
for (let index = 0; index < chunkExternalIds.length; index++) {
|
||||
const element = chunkExternalIds[index];
|
||||
const res: AdB2cResponse = await this.graphClient
|
||||
.api(`users/`)
|
||||
.select(["id", "displayName", "identities"])
|
||||
.filter(`id in (${element.map((y) => `'${y}'`).join(",")})`)
|
||||
.get();
|
||||
|
||||
b2cUsers.push(...res.value);
|
||||
}
|
||||
|
||||
return b2cUsers;
|
||||
} catch (e) {
|
||||
const { statusCode } = e;
|
||||
if (statusCode === 429) {
|
||||
throw new Adb2cTooManyRequestsError();
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const splitArrayInChunksOfFifteen = (arr: string[]): string[][] => {
|
||||
const result: string[][] = [];
|
||||
const chunkSize = 15; // SDKの制限数
|
||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||
result.push(arr.slice(i, i + chunkSize));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
147
dictation_function/src/adb2c/adb2c.ts
Normal file
147
dictation_function/src/adb2c/adb2c.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import { ClientSecretCredential } from "@azure/identity";
|
||||
import { Client } from "@microsoft/microsoft-graph-client";
|
||||
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
|
||||
import { AdB2cResponse, AdB2cUser } from "./types/types";
|
||||
import { error } from "console";
|
||||
import { makeADB2CKey, restoreAdB2cID } from "../common/cache";
|
||||
import { promisify } from "util";
|
||||
import { createRedisClient } from "../redis/redis";
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
|
||||
export class Adb2cTooManyRequestsError extends Error {}
|
||||
|
||||
export class AdB2cService {
|
||||
private graphClient: Client;
|
||||
|
||||
constructor() {
|
||||
// ADB2Cへの認証情報
|
||||
if (
|
||||
!process.env.ADB2C_TENANT_ID ||
|
||||
!process.env.ADB2C_CLIENT_ID ||
|
||||
!process.env.ADB2C_CLIENT_SECRET ||
|
||||
!process.env.ADB2C_CACHE_TTL
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
const credential = new ClientSecretCredential(
|
||||
process.env.ADB2C_TENANT_ID,
|
||||
process.env.ADB2C_CLIENT_ID,
|
||||
process.env.ADB2C_CLIENT_SECRET
|
||||
);
|
||||
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
|
||||
scopes: ["https://graph.microsoft.com/.default"],
|
||||
});
|
||||
|
||||
this.graphClient = Client.initWithMiddleware({ authProvider });
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure AD B2Cからユーザ情報を取得する
|
||||
* @param externalIds 外部ユーザーID
|
||||
* @returns ユーザ情報
|
||||
*/
|
||||
async getUsers(
|
||||
context: InvocationContext,
|
||||
externalIds: string[]
|
||||
): Promise<AdB2cUser[] | undefined> {
|
||||
const redisClient = createRedisClient();
|
||||
try {
|
||||
const b2cUsers: AdB2cUser[] = [];
|
||||
const keys = externalIds.map((externalId) => makeADB2CKey(externalId));
|
||||
|
||||
// 取得対象が0件だとmgetがエラーとなるので、1件以上のときに処理
|
||||
if (keys.length !== 0) {
|
||||
// redisからキャッシュされているユーザーを取得
|
||||
let cacheUserObjects: {
|
||||
key: string;
|
||||
value: AdB2cUser;
|
||||
}[] = [];
|
||||
const mgetAsync = promisify(redisClient.mget).bind(redisClient);
|
||||
try {
|
||||
const values = await mgetAsync(...keys);
|
||||
cacheUserObjects = values.map((value, index) => ({
|
||||
key: keys[index],
|
||||
value: value ? JSON.parse(value) : null,
|
||||
}));
|
||||
context.log("mget Result:", cacheUserObjects);
|
||||
} catch (error) {
|
||||
context.error("mget Error:", error);
|
||||
}
|
||||
|
||||
// キャッシュ上に存在していれば、キャッシュから取得する
|
||||
const cachedUsers = cacheUserObjects.flatMap((x) =>
|
||||
x.value ? [x.value] : []
|
||||
);
|
||||
if (cachedUsers.length > 0) {
|
||||
context.log(
|
||||
`[CACHE HIT] ids: ${cachedUsers.map((x) => x.id).join(",")}`
|
||||
);
|
||||
}
|
||||
|
||||
// キャッシュ上に存在していなければ、ADB2Cから取得する
|
||||
const queryExternalIds = cacheUserObjects
|
||||
.filter((x) => x.value === null)
|
||||
.map((x) => restoreAdB2cID(x.key));
|
||||
const chunkExternalIds = splitArrayInChunksOfFifteen(queryExternalIds);
|
||||
|
||||
for (let index = 0; index < chunkExternalIds.length; index++) {
|
||||
const element = chunkExternalIds[index];
|
||||
const res: AdB2cResponse = await this.graphClient
|
||||
.api(`users/`)
|
||||
.select(["id", "displayName", "identities"])
|
||||
.filter(`id in (${element.map((y) => `'${y}'`).join(",")})`)
|
||||
.get();
|
||||
|
||||
b2cUsers.push(...res.value);
|
||||
|
||||
// 取得したユーザーをキャッシュに保存する
|
||||
const users = res.value.map((user) => {
|
||||
const key = makeADB2CKey(user.id);
|
||||
return {
|
||||
key: key,
|
||||
value: user,
|
||||
};
|
||||
});
|
||||
try {
|
||||
const setexAsync = promisify(redisClient.setex).bind(redisClient);
|
||||
const ttl = process.env.ADB2C_CACHE_TTL;
|
||||
users.map(async (x) => {
|
||||
await setexAsync(x.key, ttl, JSON.stringify(x.value));
|
||||
context.log(
|
||||
"setex Result:",
|
||||
`key:${x.key},ttl:${ttl},value:${JSON.stringify(x.value)}`
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
context.error("setex Error:", error);
|
||||
}
|
||||
|
||||
context.log(
|
||||
`[ADB2C GET] externalIds: ${res.value?.map((x) => x.id).join(",")}`
|
||||
);
|
||||
}
|
||||
|
||||
return [...cachedUsers, ...b2cUsers];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
const { statusCode } = e;
|
||||
if (statusCode === 429) {
|
||||
throw new Adb2cTooManyRequestsError();
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
redisClient.quit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const splitArrayInChunksOfFifteen = (arr: string[]): string[][] => {
|
||||
const result: string[][] = [];
|
||||
const chunkSize = 15; // SDKの制限数
|
||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||
result.push(arr.slice(i, i + chunkSize));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@ -16,8 +16,8 @@ import {
|
||||
} from "../common/types/types";
|
||||
import { createMailContentOfLicenseShortage } from "../sendgrid/mailContents/U103ShortageAlert";
|
||||
import { createMailContentOfLicenseExpiringSoon } from "../sendgrid/mailContents/U104ExpiringSoonAlert";
|
||||
import { AdB2cService } from "../adb2c/adb2c.service";
|
||||
import { SendGridService } from "../sendgrid/sendgrid.service";
|
||||
import { AdB2cService } from "../adb2c/adb2c";
|
||||
import { SendGridService } from "../sendgrid/sendgrid";
|
||||
import { getMailFrom } from "../common/getEnv/getEnv";
|
||||
|
||||
export async function licenseAlertProcessing(
|
||||
@ -156,8 +156,12 @@ export async function licenseAlertProcessing(
|
||||
externalIds.push(x.secondaryAdminExternalId);
|
||||
}
|
||||
});
|
||||
const adb2cUsers = await adb2c.getUsers(externalIds);
|
||||
|
||||
const adb2cUsers = await adb2c.getUsers(context, externalIds);
|
||||
if (!adb2cUsers) {
|
||||
context.log("Target user not found");
|
||||
context.log("[OUT]licenseAlertProcessing");
|
||||
return;
|
||||
}
|
||||
// ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ
|
||||
sendTargetAccounts.map((info) => {
|
||||
const primaryAdminUser = adb2cUsers.find(
|
||||
|
||||
29
dictation_function/src/functions/redisTimerTest.ts
Normal file
29
dictation_function/src/functions/redisTimerTest.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { app, InvocationContext, Timer } from "@azure/functions";
|
||||
import * as dotenv from "dotenv";
|
||||
import { promisify } from "util";
|
||||
import { createRedisClient } from "../redis/redis";
|
||||
|
||||
export async function redisTimerTest(
|
||||
myTimer: Timer,
|
||||
context: InvocationContext
|
||||
): Promise<void> {
|
||||
context.log("---Timer function processed request.");
|
||||
|
||||
dotenv.config({ path: ".env" });
|
||||
dotenv.config({ path: ".env.local", override: true });
|
||||
|
||||
const redisClient = createRedisClient();
|
||||
const setAsync = promisify(redisClient.set).bind(redisClient);
|
||||
const getAsync = promisify(redisClient.get).bind(redisClient);
|
||||
|
||||
await setAsync("foo", "bar");
|
||||
const value = await getAsync("foo");
|
||||
context.log(`value=${value}`); // returns 'bar'
|
||||
|
||||
await redisClient.quit;
|
||||
}
|
||||
|
||||
app.timer("redisTimerTest", {
|
||||
schedule: "*/30 * * * * *",
|
||||
handler: redisTimerTest,
|
||||
});
|
||||
35
dictation_function/src/redis/redis.ts
Normal file
35
dictation_function/src/redis/redis.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { createClient, RedisClient } from "redis";
|
||||
import { error } from "console";
|
||||
|
||||
export const createRedisClient = (): RedisClient => {
|
||||
if (
|
||||
!process.env.REDIS_HOST ||
|
||||
!process.env.REDIS_PORT ||
|
||||
!process.env.REDIS_PASSWORD
|
||||
) {
|
||||
throw error("Required environment variables are not set.");
|
||||
}
|
||||
|
||||
const host = process.env.REDIS_HOST;
|
||||
const port = parseInt(process.env.REDIS_PORT, 10);
|
||||
const password = process.env.REDIS_PASSWORD;
|
||||
|
||||
let client: RedisClient;
|
||||
if (process.env.STAGE === "local") {
|
||||
client = createClient({
|
||||
host: host,
|
||||
port: port,
|
||||
password: password,
|
||||
});
|
||||
} else {
|
||||
client = createClient({
|
||||
url: `rediss://${host}:${port}`,
|
||||
password: password,
|
||||
tls: {},
|
||||
});
|
||||
}
|
||||
|
||||
client.on("error", (err) => console.log("Redis Client Error", err));
|
||||
|
||||
return client;
|
||||
};
|
||||
@ -10,8 +10,8 @@ import {
|
||||
} from "../common/types/types";
|
||||
import { AdB2cUser } from "../adb2c/types/types";
|
||||
import { ADB2C_SIGN_IN_TYPE } from "../constants";
|
||||
import { SendGridService } from "../sendgrid/sendgrid.service";
|
||||
import { AdB2cService } from "../adb2c/adb2c.service";
|
||||
import { SendGridService } from "../sendgrid/sendgrid";
|
||||
import { AdB2cService } from "../adb2c/adb2c";
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
|
||||
describe("licenseAlert", () => {
|
||||
@ -209,7 +209,10 @@ export class AdB2cServiceMock {
|
||||
* @param externalIds 外部ユーザーID
|
||||
* @returns ユーザ情報
|
||||
*/
|
||||
async getUsers(externalIds: string[]): Promise<AdB2cUser[]> {
|
||||
async getUsers(
|
||||
context: InvocationContext,
|
||||
externalIds: string[]
|
||||
): Promise<AdB2cUser[]> {
|
||||
const AdB2cMockUsers: AdB2cUser[] = [
|
||||
{
|
||||
id: "external_id1",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user