feat(foundry): support restricted subject aspects

This commit is contained in:
Codex
2026-07-24 09:31:34 +03:00
parent 03aa9e3e7e
commit 49f0c449c1
13 changed files with 313 additions and 15 deletions
+2
View File
@@ -115,6 +115,7 @@ export type MapDataProductBinding = {
subjectDetailProfileId?: string;
aspectId?: string;
joinToBindingId?: string;
dataClass?: "operational" | "restricted";
};
export type MapSubjectDetailProfile = {
@@ -532,6 +533,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
fact,
bindingId: binding.id,
dataProductId: binding.dataProductId,
dataClass: binding.dataClass ?? "operational",
}]];
}));
return buildMapSubjectCardModel(entity.fact, {
+32 -6
View File
@@ -1,4 +1,5 @@
const SECRET_LIKE = /(?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key|imei|phone|decrypt|address|raw[_-]?params?)/i;
const SECRET_MATERIAL = /(?:token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key|decrypt|raw[_-]?params?)/i;
const RESTRICTED_IDENTIFIER = /(?:imei|phone|telephone|address)/i;
const DIRECT_IDENTIFIER_VALUE = /^(?:\+?\d[\d ()-]{8,18}|\d{14,16})$/;
export const DEFAULT_MAP_SUBJECT_DETAIL_PROFILE = Object.freeze({
@@ -106,7 +107,12 @@ export const DEFAULT_MAP_SUBJECT_DETAIL_PROFILE = Object.freeze({
export function buildMapSubjectCardModel(fact, context = {}) {
if (!isPlainObject(fact)) return null;
const profile = isPlainObject(context.profile) ? context.profile : DEFAULT_MAP_SUBJECT_DETAIL_PROFILE;
const primary = { fact, dataProductId: context.dataProductId, bindingId: context.bindingId };
const primary = {
fact,
dataProductId: context.dataProductId,
bindingId: context.bindingId,
dataClass: context.dataClass ?? "operational",
};
const aspects = { primary, ...(isPlainObject(context.aspects) ? context.aspects : {}) };
const tabs = Array.isArray(profile.tabs) ? profile.tabs.flatMap((tab) => buildTab(tab, aspects)) : [];
if (!tabs.length) return null;
@@ -138,15 +144,24 @@ function buildSection(section, aspects) {
const rows = [];
const readings = [];
for (const field of Array.isArray(section.fields) ? section.fields : []) {
if (!isPlainObject(field) || SECRET_LIKE.test(String(field.field || "")) || SECRET_LIKE.test(String(field.label || ""))) continue;
if (!isPlainObject(field)
|| SECRET_MATERIAL.test(String(field.field || ""))
|| SECRET_MATERIAL.test(String(field.label || ""))) continue;
const aspect = aspects[stringValue(field.aspectId) || "primary"];
if (!isPlainObject(aspect) || !isPlainObject(aspect.fact)) continue;
const restricted = field.dataClass === "restricted" && aspect.dataClass === "restricted";
if (
(RESTRICTED_IDENTIFIER.test(String(field.field || ""))
|| RESTRICTED_IDENTIFIER.test(String(field.label || "")))
&& !restricted
) continue;
const value = resolveFieldValue(aspect, field);
if (field.format === "telemetry_readings") {
readings.push(...normalizeTelemetryReadings(value, field.allowedReadingIds));
continue;
}
if (value === undefined || value === null || value === "") continue;
if (typeof value === "string" && DIRECT_IDENTIFIER_VALUE.test(value.trim()) && !restricted) continue;
rows.push({
key: String(field.id || `${field.source}.${field.field}`),
label: stringValue(field.label),
@@ -171,13 +186,21 @@ function resolveFieldValue(aspect, field) {
export function normalizeTelemetryReadings(value, allowedReadingIds = []) {
if (!Array.isArray(value) || !Array.isArray(allowedReadingIds) || allowedReadingIds.length === 0) return [];
const allowed = new Set(allowedReadingIds.filter((item) => safeIdentifier(item) && !SECRET_LIKE.test(item)));
const allowed = new Set(allowedReadingIds.filter(
(item) => safeIdentifier(item) && !SECRET_MATERIAL.test(item) && !RESTRICTED_IDENTIFIER.test(item),
));
const seen = new Set();
return value.slice(0, 128).flatMap((candidate) => {
if (!isPlainObject(candidate)) return [];
const id = stringValue(candidate.id);
const label = stringValue(candidate.label);
if (!allowed.has(id) || !label || seen.has(id) || SECRET_LIKE.test(id) || SECRET_LIKE.test(label)) return [];
if (!allowed.has(id)
|| !label
|| seen.has(id)
|| SECRET_MATERIAL.test(id)
|| SECRET_MATERIAL.test(label)
|| RESTRICTED_IDENTIFIER.test(id)
|| RESTRICTED_IDENTIFIER.test(label)) return [];
if (!isSafeScalar(candidate.value)) return [];
seen.add(id);
const unit = stringValue(candidate.unit).slice(0, 32);
@@ -217,7 +240,10 @@ function isSafeScalar(value) {
if (typeof value === "number") return Number.isFinite(value) && (!Number.isInteger(value) || Math.abs(value) < 1_000_000_000_000);
if (typeof value !== "string") return false;
const normalized = value.trim();
return normalized.length <= 512 && !SECRET_LIKE.test(normalized) && !DIRECT_IDENTIFIER_VALUE.test(normalized);
return normalized.length <= 512
&& !SECRET_MATERIAL.test(normalized)
&& !RESTRICTED_IDENTIFIER.test(normalized)
&& !DIRECT_IDENTIFIER_VALUE.test(normalized);
}
function safeIdentifier(value) {
@@ -305,6 +305,7 @@ export function useMapDataProductRuntime({
semanticTypes: binding.semanticTypes,
fieldProjection: binding.fieldProjection,
presentationProfileId: binding.presentationProfileId,
dataClass: binding.dataClass ?? "operational",
}))),
[bindings],
);