forked from baron/baron-sso
개요 페이지 클레임 변경 내용 표현
This commit is contained in:
@@ -91,6 +91,117 @@ describe("recent client changes", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores audit object key order changes in update details", () => {
|
||||||
|
mockLocale("ko");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildRecentClientChangeDetails("UPDATE_CLIENT", {
|
||||||
|
before: {
|
||||||
|
id_token_claims: [
|
||||||
|
{
|
||||||
|
key: "license",
|
||||||
|
namespace: "rp_claims",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
value: "",
|
||||||
|
valueType: "text",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "date",
|
||||||
|
namespace: "rp_claims",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
value: "",
|
||||||
|
valueType: "date",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
id_token_claims: [
|
||||||
|
{
|
||||||
|
namespace: "rp_claims",
|
||||||
|
key: "license",
|
||||||
|
value: "",
|
||||||
|
valueType: "text",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
namespace: "rp_claims",
|
||||||
|
key: "date",
|
||||||
|
value: "",
|
||||||
|
valueType: "date",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("summarizes id_token_claims additions and removals", () => {
|
||||||
|
mockLocale("ko");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildRecentClientChangeDetails("UPDATE_CLIENT", {
|
||||||
|
before: {
|
||||||
|
id_token_claims: [
|
||||||
|
{
|
||||||
|
namespace: "rp_claims",
|
||||||
|
key: "license",
|
||||||
|
value: "",
|
||||||
|
valueType: "text",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
namespace: "rp_claims",
|
||||||
|
key: "date",
|
||||||
|
value: "",
|
||||||
|
valueType: "date",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
id_token_claims: [
|
||||||
|
{
|
||||||
|
namespace: "rp_claims",
|
||||||
|
key: "license",
|
||||||
|
value: "",
|
||||||
|
valueType: "text",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
namespace: "rp_claims",
|
||||||
|
key: "test",
|
||||||
|
value: "",
|
||||||
|
valueType: "text",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
label: "커스텀 클레임",
|
||||||
|
value: "+ test (text), - date (date)",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("builds recent client changes with sorting, filtering, and detail slicing", () => {
|
it("builds recent client changes with sorting, filtering, and detail slicing", () => {
|
||||||
mockLocale("ko");
|
mockLocale("ko");
|
||||||
|
|
||||||
@@ -192,6 +303,40 @@ describe("recent client changes", () => {
|
|||||||
after: { name: "Ignored" },
|
after: { name: "Ignored" },
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
makeAuditLog(
|
||||||
|
"evt-9",
|
||||||
|
"2026-05-27T15:00:00.000Z",
|
||||||
|
"UPDATE_CLIENT",
|
||||||
|
"client-a",
|
||||||
|
{
|
||||||
|
before: {
|
||||||
|
id_token_claims: [
|
||||||
|
{
|
||||||
|
key: "license",
|
||||||
|
namespace: "rp_claims",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
value: "",
|
||||||
|
valueType: "text",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
id_token_claims: [
|
||||||
|
{
|
||||||
|
namespace: "rp_claims",
|
||||||
|
key: "license",
|
||||||
|
value: "",
|
||||||
|
valueType: "text",
|
||||||
|
nullable: true,
|
||||||
|
readPermission: "admin_only",
|
||||||
|
writePermission: "admin_only",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const changes = buildRecentClientChanges(auditLogs, clients);
|
const changes = buildRecentClientChanges(auditLogs, clients);
|
||||||
|
|||||||
@@ -33,6 +33,27 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAuditValue(value: unknown): unknown {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => normalizeAuditValue(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRecord(value)) {
|
||||||
|
return Object.keys(value)
|
||||||
|
.sort()
|
||||||
|
.reduce<Record<string, unknown>>((acc, key) => {
|
||||||
|
acc[key] = normalizeAuditValue(value[key]);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditValueSignature(value: unknown) {
|
||||||
|
return JSON.stringify(normalizeAuditValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
export function getRecentClientActionLabel(action: string) {
|
export function getRecentClientActionLabel(action: string) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "CREATE_CLIENT":
|
case "CREATE_CLIENT":
|
||||||
@@ -74,11 +95,169 @@ function getRecentClientFieldLabel(key: string) {
|
|||||||
"ui.dev.clients.details.credentials.client_secret",
|
"ui.dev.clients.details.credentials.client_secret",
|
||||||
"클라이언트 시크릿",
|
"클라이언트 시크릿",
|
||||||
);
|
);
|
||||||
|
case "id_token_claims":
|
||||||
|
return t(
|
||||||
|
"ui.dev.clients.general.id_token_claims.title",
|
||||||
|
"Custom Claims",
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getIdTokenClaimIdentity(claim: Record<string, unknown>) {
|
||||||
|
const namespace =
|
||||||
|
typeof claim.namespace === "string" && claim.namespace
|
||||||
|
? claim.namespace
|
||||||
|
: null;
|
||||||
|
const key = typeof claim.key === "string" && claim.key ? claim.key : null;
|
||||||
|
|
||||||
|
if (!namespace || !key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { namespace, key };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIdTokenClaimDisplayName(claim: Record<string, unknown>) {
|
||||||
|
const identity = getIdTokenClaimIdentity(claim);
|
||||||
|
if (!identity) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
if (identity.namespace === "rp_claims") {
|
||||||
|
return identity.key;
|
||||||
|
}
|
||||||
|
return `${identity.namespace}:${identity.key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSimpleAuditScalar(value: unknown) {
|
||||||
|
return (
|
||||||
|
value === null ||
|
||||||
|
value === undefined ||
|
||||||
|
typeof value === "string" ||
|
||||||
|
typeof value === "number" ||
|
||||||
|
typeof value === "boolean"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIdTokenClaimChangeSummary(
|
||||||
|
beforeValue: unknown,
|
||||||
|
afterValue: unknown,
|
||||||
|
) {
|
||||||
|
if (!isRecord(beforeValue) || !isRecord(afterValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeDisplayName = formatIdTokenClaimDisplayName(beforeValue);
|
||||||
|
const afterDisplayName = formatIdTokenClaimDisplayName(afterValue);
|
||||||
|
if (beforeDisplayName !== afterDisplayName) {
|
||||||
|
return `~ ${beforeDisplayName} → ${afterDisplayName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeValueType =
|
||||||
|
typeof beforeValue.valueType === "string" ? beforeValue.valueType : null;
|
||||||
|
const afterValueType =
|
||||||
|
typeof afterValue.valueType === "string" ? afterValue.valueType : null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
beforeValueType &&
|
||||||
|
afterValueType &&
|
||||||
|
beforeValueType !== afterValueType
|
||||||
|
) {
|
||||||
|
return `~ ${beforeDisplayName}: ${beforeValueType} → ${afterValueType}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeScalar = beforeValue.value;
|
||||||
|
const afterScalar = afterValue.value;
|
||||||
|
if (
|
||||||
|
isSimpleAuditScalar(beforeScalar) &&
|
||||||
|
isSimpleAuditScalar(afterScalar) &&
|
||||||
|
formatAuditValue(beforeScalar) !== formatAuditValue(afterScalar)
|
||||||
|
) {
|
||||||
|
return `~ ${beforeDisplayName}: ${formatAuditValue(beforeScalar)} → ${formatAuditValue(afterScalar)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `~ ${beforeDisplayName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeIdTokenClaimArrayChange(
|
||||||
|
beforeValue: unknown,
|
||||||
|
afterValue: unknown,
|
||||||
|
) {
|
||||||
|
if (!Array.isArray(beforeValue) || !Array.isArray(afterValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeClaims = beforeValue.filter(isRecord);
|
||||||
|
const afterClaims = afterValue.filter(isRecord);
|
||||||
|
const beforeByIdentity = new Map<string, Record<string, unknown>>();
|
||||||
|
const afterByIdentity = new Map<string, Record<string, unknown>>();
|
||||||
|
|
||||||
|
for (const claim of beforeClaims) {
|
||||||
|
const identity = getIdTokenClaimIdentity(claim);
|
||||||
|
if (identity) {
|
||||||
|
beforeByIdentity.set(`${identity.namespace}:${identity.key}`, claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const claim of afterClaims) {
|
||||||
|
const identity = getIdTokenClaimIdentity(claim);
|
||||||
|
if (identity) {
|
||||||
|
afterByIdentity.set(`${identity.namespace}:${identity.key}`, claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const additions: string[] = [];
|
||||||
|
const removals: string[] = [];
|
||||||
|
const updates: string[] = [];
|
||||||
|
|
||||||
|
for (const [identity, afterClaim] of afterByIdentity.entries()) {
|
||||||
|
const beforeClaim = beforeByIdentity.get(identity);
|
||||||
|
const displayName = formatIdTokenClaimDisplayName(afterClaim);
|
||||||
|
|
||||||
|
if (!beforeClaim) {
|
||||||
|
const valueType =
|
||||||
|
typeof afterClaim.valueType === "string" ? afterClaim.valueType : null;
|
||||||
|
additions.push(
|
||||||
|
valueType ? `+ ${displayName} (${valueType})` : `+ ${displayName}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auditValueSignature(beforeClaim) === auditValueSignature(afterClaim)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = formatIdTokenClaimChangeSummary(beforeClaim, afterClaim);
|
||||||
|
if (summary) {
|
||||||
|
updates.push(summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [identity, beforeClaim] of beforeByIdentity.entries()) {
|
||||||
|
if (afterByIdentity.has(identity)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const displayName = formatIdTokenClaimDisplayName(beforeClaim);
|
||||||
|
const valueType =
|
||||||
|
typeof beforeClaim.valueType === "string" ? beforeClaim.valueType : null;
|
||||||
|
removals.push(
|
||||||
|
valueType ? `- ${displayName} (${valueType})` : `- ${displayName}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [...additions, ...removals, ...updates].slice(0, 4);
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additions.length + removals.length + updates.length > parts.length) {
|
||||||
|
parts.push("...");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
export function buildRecentClientChangeDetails(
|
export function buildRecentClientChangeDetails(
|
||||||
action: string,
|
action: string,
|
||||||
details: AuditDetails,
|
details: AuditDetails,
|
||||||
@@ -126,8 +305,19 @@ export function buildRecentClientChangeDetails(
|
|||||||
const beforeValue = before[key];
|
const beforeValue = before[key];
|
||||||
const afterValue = after[key];
|
const afterValue = after[key];
|
||||||
|
|
||||||
|
if (key === "id_token_claims") {
|
||||||
|
const value = summarizeIdTokenClaimArrayChange(beforeValue, afterValue);
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: getRecentClientFieldLabel(key),
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action !== "CREATE_CLIENT" && action !== "DELETE_CLIENT") {
|
if (action !== "CREATE_CLIENT" && action !== "DELETE_CLIENT") {
|
||||||
if (JSON.stringify(beforeValue) === JSON.stringify(afterValue)) {
|
if (auditValueSignature(beforeValue) === auditValueSignature(afterValue)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,6 +351,10 @@ export function buildRecentClientChangeDetails(
|
|||||||
})
|
})
|
||||||
.filter((item): item is { label: string; value: string } => Boolean(item));
|
.filter((item): item is { label: string; value: string } => Boolean(item));
|
||||||
|
|
||||||
|
if (changes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return changes.slice(0, 3);
|
return changes.slice(0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +388,12 @@ export function buildRecentClientChanges(
|
|||||||
detailLabels: buildRecentClientChangeDetails(action, details),
|
detailLabels: buildRecentClientChangeDetails(action, details),
|
||||||
} satisfies RecentClientChange;
|
} satisfies RecentClientChange;
|
||||||
})
|
})
|
||||||
.filter((item): item is RecentClientChange => Boolean(item))
|
.filter((item): item is RecentClientChange => {
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return item.detailLabels.length > 0;
|
||||||
|
})
|
||||||
.sort(
|
.sort(
|
||||||
(left, right) =>
|
(left, right) =>
|
||||||
new Date(right.timestamp).getTime() -
|
new Date(right.timestamp).getTime() -
|
||||||
|
|||||||
Reference in New Issue
Block a user