feat(foundry): support restricted subject aspects
This commit is contained in:
@@ -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, {
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user