Files
threshold-ui/ui/admin/app/utils/grant-parser.js
T
Lisbet Alvarez 7cddf7cb91 Refactor utils to share components (#3279)
* refactor: 💡 initial refactor for utils to share components

* refactor: 💡 actions logic

* Add missing copyright headers

* refactor: 💡 simplify actions getter logic

* refactor: 💡 convert output fields logic into a separate getter

* test: 💍 fix unit test

* fix: 🐛 bug with unknown fields lint error

* refactor: 💡 pr feedback - simplify template check

* refactor: 💡 remove unnecessary unit test file
2026-05-29 16:52:00 -07:00

280 lines
7.6 KiB
JavaScript

/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export const GRANT_FIELDS = ['ids', 'type', 'actions', 'output_fields'];
export const TEMPLATE_RESOURCE_TYPES = {
'{{.User.Id}}': 'user',
'{{.Account.Id}}': 'account',
'{{user.id}}': 'user',
'{{account.id}}': 'account',
};
export const normalizeGrantsSchema = (grantsSchema) => {
const resourceTypes = grantsSchema?.resource_types ?? [];
const resourcesByType = resourceTypes.reduce((resources, resourceType) => {
const collectionActions = resourceType.collection_actions ?? [];
const idActions = resourceType.id_actions ?? [];
const idPrefixes = resourceType.id_prefixes ?? [];
resources[resourceType.type] = {
actions: [...collectionActions, ...idActions],
collectionActions,
idActions,
idPrefixes,
parentType: resourceType.parent_type,
outputFields: resourceType.output_fields ?? [],
};
return resources;
}, {});
const validResourceTypes = resourceTypes.map((resource) => resource.type);
validResourceTypes.push('*');
const allActions = new Set();
Object.values(resourcesByType).forEach((resource) => {
resource.actions.forEach((action) => allActions.add(action));
});
const validActions = Array.from(allActions);
const resourceTypesByIdPrefix = resourceTypes.reduce((map, resource) => {
resource.id_prefixes?.forEach((prefix) => {
map[prefix] = resource.type;
});
return map;
}, {});
let parentTypes = new Set(
resourceTypes.filter((r) => r.parent_type).map((r) => r.parent_type),
);
parentTypes = Array.from(parentTypes);
const topLevelTypes = resourceTypes
.filter((r) => {
const hasCreateAction = r.collection_actions.includes('create');
const hasListAction = r.collection_actions.includes('list');
return !r.parent_type && (hasCreateAction || hasListAction);
})
.map((r) => r.type);
const childResourceTypesByParentType = resourceTypes.reduce(
(childrenByParentType, resourceType) => {
const parentType = resourceType.parent_type;
if (!parentType) {
return childrenByParentType;
}
childrenByParentType[parentType] = [
...(childrenByParentType[parentType] ?? []),
resourceType.type,
];
return childrenByParentType;
},
{},
);
return {
resourcesByType,
resourceTypesByIdPrefix,
childResourceTypesByParentType,
parentTypes,
topLevelTypes,
validResourceTypes,
validActions,
};
};
export const parseGrantLine = (lineText = '', lineStart = 0) => {
const pairs = lineText.split(';').filter((p) => p.trim());
const fields = [];
const fieldMap = {};
const fieldPositions = {};
const fieldKeyCount = {};
let pairOffset = 0;
for (const pair of pairs) {
const pairInLine = lineText.indexOf(pair, pairOffset);
const [key, ...valueParts] = pair.split('=');
const value = valueParts.join('=');
const trimmedKey = key.trim();
const keyStartInPair = pair.indexOf(trimmedKey);
const valueStartInPair = pair.indexOf('=') + 1;
const pos = {
keyStart: lineStart + pairInLine + keyStartInPair,
valueStart: lineStart + pairInLine + valueStartInPair,
valueEnd: lineStart + pairInLine + pair.length,
};
const hasValue = valueParts.length > 0;
fields.push({
key: trimmedKey,
value: hasValue ? value.trim() : undefined,
pos,
});
fieldKeyCount[trimmedKey] = (fieldKeyCount[trimmedKey] ?? 0) + 1;
if (hasValue && trimmedKey) {
fieldPositions[trimmedKey] = pos;
const trimmedValue = value.trim();
if (trimmedValue) {
fieldMap[trimmedKey] = trimmedValue;
}
}
pairOffset = pairInLine + pair.length + 1;
}
return { fields, fieldMap, fieldPositions, fieldKeyCount };
};
const getChildResourceValues = (schema, compatibleType, getValues) => {
if (
!compatibleType ||
!schema.childResourceTypesByParentType[compatibleType]?.length
) {
return [];
}
const childTypes =
schema.childResourceTypesByParentType[compatibleType] ?? [];
return [
...new Set(
childTypes.flatMap((type) => getValues(schema.resourcesByType[type])),
),
];
};
const getChildResourceActions = (schema, compatibleType) =>
getChildResourceValues(
schema,
compatibleType,
(resource) => resource?.actions ?? [],
);
const getChildResourceOutputFields = (schema, compatibleType) =>
getChildResourceValues(
schema,
compatibleType,
(resource) => resource?.outputFields ?? [],
);
const withWildCard = (values = []) => ['*', ...new Set(values)];
export const getValidActions = (
schema,
{ typeValue, idsValue, idsWildcard, idsKnownType },
) => {
const typeWildcard = typeValue === '*';
const hasExplicitType = Boolean(typeValue) && !typeWildcard;
if (idsValue) {
// No type, ids resolve to a known type → that type's id actions.
if (!typeValue && idsKnownType) {
return {
actionsType: 'id',
actions: withWildCard(
schema.resourcesByType[idsKnownType]?.idActions ?? [],
),
};
}
// Wildcard type, known id type → child resource actions.
if (typeWildcard && idsKnownType) {
return {
actionsType: 'child',
actions: withWildCard(getChildResourceActions(schema, idsKnownType)),
};
}
// Explicit type → validate parent relationship then return type actions.
if (hasExplicitType) {
if (idsKnownType) {
const selectedResource = schema.resourcesByType[typeValue];
if (selectedResource?.parentType !== idsKnownType) {
return { actionsType: 'invalid', actions: [] };
}
}
return {
actionsType: 'type',
actions: withWildCard(schema.resourcesByType[typeValue]?.actions ?? []),
};
}
// Wildcard ids with no explicit type → all actions.
if (idsWildcard && !hasExplicitType) {
return { actionsType: 'all', actions: withWildCard(schema.validActions) };
}
} else {
// No ids and no type → all actions.
if (!typeValue) {
return { actionsType: 'all', actions: withWildCard(schema.validActions) };
}
// No ids, explicit top-level type → collection actions.
if (hasExplicitType && schema.topLevelTypes.includes(typeValue)) {
return {
actionsType: 'collection',
actions: schema.resourcesByType[typeValue]?.collectionActions ?? [],
};
}
}
return { actionsType: 'invalid', actions: [] };
};
export const getValidOutputFields = (schema, { typeValue, idsKnownType }) => {
const typeWildcard = typeValue === '*';
const hasExplicitType = Boolean(typeValue) && !typeWildcard;
if (hasExplicitType) {
const resourceOutputFields =
schema.resourcesByType[typeValue]?.outputFields ?? [];
return {
outputFieldsType: 'type',
outputFields: resourceOutputFields.length
? withWildCard(resourceOutputFields)
: ['*'],
resolvedType: typeValue,
};
}
if (typeWildcard && idsKnownType) {
const childOutputFields = getChildResourceOutputFields(
schema,
idsKnownType,
);
return {
outputFieldsType: 'child',
outputFields: childOutputFields.length
? withWildCard(childOutputFields)
: ['*'],
resolvedType: null,
};
}
if (!typeValue && idsKnownType) {
const resourceOutputFields =
schema.resourcesByType[idsKnownType]?.outputFields ?? [];
return {
outputFieldsType: 'type',
outputFields: resourceOutputFields.length
? withWildCard(resourceOutputFields)
: ['*'],
resolvedType: idsKnownType,
};
}
return { outputFieldsType: 'all', outputFields: ['*'], resolvedType: null };
};