@lcap/nasl
Version:
NetEase Application Specific Language
729 lines • 30.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.testWithOldPermissionResult = exports.sortResourceDtoMap = exports.genLogicAuthFlag = exports.genPermissionData = exports.genPermissionDataOld = exports.getAllLogics = void 0;
const concepts_1 = require("../concepts");
const nasl_concepts_1 = require("@lcap/nasl-concepts");
const { processToTreeFragment } = concepts_1.service;
function isReadOnly(logic) {
return !!logic.module || logic.parentNode?.concept === 'Namespace' || logic.parentKey === 'readonly'; // process logic
}
const cache = new Map();
let useCache = false;
async function findUsage(logic) {
if (useCache && cache.has(logic))
return cache.get(logic);
const res = isReadOnly(logic) ? await logic.findReadOnlyLogicUsage?.() : await logic.findUsage?.();
if (useCache)
cache.set(logic, res);
return res;
}
function openCache() {
useCache = true;
cache.clear();
}
function closeCache() {
useCache = false;
cache.clear();
}
async function findViewLogicReferences(logic, vis = new Set()) {
const usageMap = await findUsage(logic);
const usages = [];
usageMap?.forEach((usage, node) => {
if (node instanceof concepts_1.FrontendType)
usages.push(usage);
});
async function dfs(usage) {
if (usage.node instanceof concepts_1.CallLogic) {
const logic = usage.node?.logic || usage.node?.getAncestor?.('BusinessLogic');
if (!vis.has(logic) && logic) {
vis.add(logic);
await findViewLogicReferences(logic, vis);
}
}
if (usage.children) {
for (const child of usage.children) {
await dfs(child);
}
}
}
for (const usage of usages) {
await dfs(usage);
}
return Array.from(vis);
}
async function findUIReferences(logic) {
const usageMap = await findUsage(logic);
const usages = [];
usageMap.forEach((usage, node) => {
if (node instanceof concepts_1.FrontendType)
usages.push(usage);
});
const res = [];
function dfs(usage, parent = null) {
if ((usage.node instanceof concepts_1.BindEvent ||
usage.node instanceof concepts_1.BindAttribute ||
usage.node instanceof concepts_1.BindStyle ||
usage.node instanceof concepts_1.BindDirective) &&
(parent.node instanceof concepts_1.View || parent.node instanceof concepts_1.BusinessComponent || parent.node instanceof concepts_1.ViewElement || parent.node instanceof concepts_1.Frontend || parent.node instanceof concepts_1.FrontendType))
res.push(parent.node);
usage.children?.forEach((child) => {
dfs(child, usage);
});
}
usages.forEach((usage) => {
dfs(usage);
});
return res;
}
function findResourcesOfUI(view) {
const res = [];
let node = view;
while (node) {
if (node instanceof concepts_1.View && node.auth)
res.push({
path: node.authPath,
type: 'page',
});
else if (node instanceof concepts_1.ViewElement && node.auth) {
res.push({
path: node.authPath,
type: 'component',
});
}
node = node.parentNode;
}
return res;
}
function optimizeResourceData(resources) {
const res = removeRedundantResourceData(resources);
return res;
}
// [['b'], ['a' ,'b']] 只需要保留 [['b']]
function removeRedundantResourceData(resources) {
const wm = new WeakMap();
for (const resource of resources) {
const hash = resource.map(({ path, type }) => `${path}_${type}`).join();
wm.set(resource, hash);
}
resources.sort((a, b) => wm.get(a).length - wm.get(b).length);
const res = [];
const hashes = [];
for (const resource of resources) {
const hash = wm.get(resource);
// h 为空字符串时,也即 resource 为空数组,表示不需要鉴权,所有用户都能调用。
if (hashes.some((h) => h === '' || hash.endsWith(`,${h}`) || hash === h))
continue;
res.push(resource);
hashes.push(hash);
}
return res;
}
async function findResourcesOfLogic(logic, uploaders) {
const logics = await findViewLogicReferences(logic);
logics.push(logic);
let UIs = [];
for (const logic of logics) {
UIs = UIs.concat(await findUIReferences(logic));
}
const service = logic.toService();
if (uploaders.has(service.url.path)) {
UIs = UIs.concat(uploaders.get(service.url.path));
}
UIs = Array.from(new Set(UIs));
const res = UIs.map((ui) => findResourcesOfUI(ui));
return optimizeResourceData(res);
}
function getAllLogics(_app) {
// @ts-expect-error
const app = _app.__v_raw ?? _app;
const modules = [];
const entities = [];
const logics = [];
if (Array.isArray(app.dataSources)) {
app.dataSources.forEach((dataSource) => {
if (Array.isArray(dataSource.entities)) {
entities.push(...dataSource.entities);
}
});
}
app.logics && logics.push(...app.logics);
app.dependencies && modules.push(...app.dependencies);
app.interfaceDependencies && modules.push(...app.interfaceDependencies);
app.connectorDependencies && modules.push(...app.connectorDependencies);
app.sharedAppDependencies && modules.push(...app.sharedAppDependencies);
modules.forEach((module) => {
module.logics && logics.push(...module.logics);
if (Array.isArray(module.dataSources)) {
module.dataSources.forEach((dataSource) => {
if (Array.isArray(dataSource.entities)) {
entities.push(...dataSource.entities);
}
});
}
// 连接器相关的字段存在于 Connector 下面的 namespace 中
if (module instanceof concepts_1.Connector) {
module.namespaces.forEach((ns) => {
if (Array.isArray(ns.logics)) {
logics.push(...ns.logics);
}
});
}
});
const allLogics = [];
if (Array.isArray(entities)) {
entities.forEach((entity) => {
const ns = entity.ns;
if (Array.isArray(ns?.logics)) {
allLogics.push(...ns.logics);
}
});
}
if (Array.isArray(logics)) {
allLogics.push(...logics);
}
const processLogics = [];
const processTreeFragments = (app.processes || []).map(processToTreeFragment);
processTreeFragments.forEach((tree) => {
processLogics.push(...tree.logics);
tree.elements.forEach((element) => {
processLogics.push(...element.logics);
});
});
return {
logics: allLogics,
processLogics,
};
}
exports.getAllLogics = getAllLogics;
function findUploaders(app) {
const uploaders = new Map();
const callLogicUploadList = new Map();
let callLogicUploadFlag = false;
app.traverseChildren((node) => {
if (node instanceof concepts_1.ViewElement && node.tag?.toLowerCase()?.includes('upload')) {
const urlAttr = node.bindAttrs.find((item) => item.name === 'action') ||
node.bindAttrs.find((item) => item.name === 'url');
const url = urlAttr?.value;
if (url) {
if (!uploaders.has(url))
uploaders.set(url, [node]);
else
uploaders.get(url).push(node);
}
}
if (node instanceof concepts_1.CallLogic && node?.calleeName?.includes('downloadFile') && node?.calleeNamespace === 'nasl.io' && !callLogicUploadFlag) {
const viewElement = node?.getAncestor('ViewElement');
const view = node?.getAncestor('View');
// ViewElement 权限优先
if (viewElement instanceof concepts_1.ViewElement && viewElement?.auth) {
callLogicUploadList.set(viewElement?.authPath, [{
type: 'component',
path: viewElement?.authPath
}]);
}
else if (view instanceof concepts_1.View && view?.auth) {
callLogicUploadList.set(view?.authPath, [{
type: 'page',
path: view?.authPath
}]);
}
else {
// 如果开启权限和未开启权限都有,以未开启权限为准,清空 map,设置为 [[]]
callLogicUploadFlag = true;
callLogicUploadList.clear();
}
}
});
return { uploaders, callLogicUploadList: callLogicUploadList.size === 0 ? [[]] : Array.from(callLogicUploadList.values()) };
}
function checkUploadAuth(uploaders) {
const res = [];
// 如果开启控制权限-component,没开启权限-page
uploaders.forEach((value, key) => {
const authValue = [];
value.forEach((node) => {
if (node instanceof concepts_1.ViewElement && node.view) {
const path = node.auth ? node.authPath : `${node.view.path}`;
const type = node.auth ? 'component' : 'page';
authValue.push({ path, type });
}
return null;
});
res.push({ key, authValue });
});
return res;
}
function checkPageAndUploadAuth(uploaders, key) {
let flag = false;
uploaders.get(key).forEach((node) => {
if (node.view instanceof concepts_1.View) {
// 当前页面未开启权限且文件上传组件也未开启权限
if (!node.view.auth && node instanceof concepts_1.ViewElement && !node.auth) {
flag = true;
}
}
else if (node.getAncestor('BusinessComponent')) {
flag = true;
}
});
return flag;
}
function convertArray(arr) {
return arr.map((item) => [item]);
}
async function genPermissionDataOld(app) {
openCache();
const logicPageResourceDtoList = {};
const { logics, processLogics } = getAllLogics(app);
const { uploaders, callLogicUploadList } = findUploaders(app);
for (const logic of [...logics, ...processLogics]) {
const resources = await findResourcesOfLogic(logic, uploaders);
if (resources.length === 0)
continue;
const service = logics.includes(logic)
? logic.toService()
: logic.toProcessService();
const key = `${service.url.path}:${service.url.method}`;
logicPageResourceDtoList[key] = resources;
}
const authVals = checkUploadAuth(uploaders);
if (authVals?.length) {
authVals.forEach((item) => {
const { key, authValue } = item;
if (key.startsWith('/upload') || key.startsWith('/v1/upload') || key.startsWith('/api')) {
logicPageResourceDtoList[`${key}:POST`] = checkPageAndUploadAuth(uploaders, key) ? [[]] : convertArray(authValue);
}
});
}
logicPageResourceDtoList['/upload/download_files:POST'] = callLogicUploadList;
logicPageResourceDtoList['/api/logics/downloadFile:POST'] = [[]];
logicPageResourceDtoList['/api/logics/downloadFile:GET'] = [[]];
closeCache();
return logicPageResourceDtoList;
}
exports.genPermissionDataOld = genPermissionDataOld;
function generateServiceKey(logic, kind, call) {
let service;
if (kind === 'server') {
service = logic.toService(call);
}
else if (kind === 'process') {
service = logic.toProcessService();
}
else {
throw new Error('Invalid service key kind');
}
const key = `${service.url.path}:${service.url.method}`;
return key;
}
function createArrayOnAdd(k, v, m) {
if (m.get(k)) {
m.get(k).push(v);
}
else {
m.set(k, [v]);
}
}
function createSetOnAdd(k, v, m) {
if (m.get(k)) {
m.get(k).add(v);
}
else {
m.set(k, new Set([v]));
}
}
// 需要区分:前端逻辑、后端逻辑、流程逻辑
// 需要的信息:后端逻辑、流程逻辑的 service path;逻辑调用到其定义的映射;
// 流程逻辑 call 的 getCallNode() 每次都不同
// 表单验证 $refs.validate 的定义缺少 calleewholeKey 且访问时会抛异常
// 不跳过 playground 草稿区内容:赫基有用到 JS代码块,在代码块里调用逻辑。JS代码块里的引用没有识别出来。但恰巧在草稿区的东西,提供了引用关系。赫基项目,删除了草稿区的东西会导致401。
function genPermissionData(_app) {
// @ts-expect-error
const app = _app.__v_raw ?? _app;
function clearCtx(ctx) {
ctx.viewElement = null;
ctx.viewElements = new Set();
ctx.thisLogic = null;
ctx.bindEvent = null;
ctx.event = null;
ctx.viewLike = null;
ctx.name2ViewElement = null;
}
let { logics: tmpLogics, processLogics: tmpProcessLogics } = getAllLogics(app);
// 调用后端逻辑的Map
const backendLogicCallCtx = new Map();
// 调用前端逻辑的Map
const frontendLogicCallCtx = [];
// from calleeWholeKey to Logic,消除流程逻辑每次找到的定义都是即时演算生成的,地址不同的问题。
const frontNdCache = new Map();
const processNdCache = new Map();
const serverNdCache = new Map();
tmpProcessLogics.forEach(l => processNdCache.set(l.calleewholeKey, l));
tmpLogics.forEach(l => serverNdCache.set(l.calleewholeKey, l));
tmpProcessLogics = null;
tmpLogics = null;
// 上传组件
const uploaders = new Map();
const callLogicUploadList = new Map();
let callLogicUploadFlag = false;
const defToCalls = new Map();
const traverseFn = ((nd, ctx) => {
switch (nd.concept) {
case 'ViewElement': {
ctx.thisLogic = null;
ctx.viewElement = nd;
ctx.viewElements = new Set(ctx.viewElements); // if use ctx.viewElements.add(node) directly, will encounter a SEVERE slow down. For example, 3s vs 12s
ctx.viewElements.add(nd);
// collect uploaders
if (nd.tag?.toLowerCase()?.includes('upload')) {
const urlAttr = nd.bindAttrs.find((item) => item.name === 'action') ||
nd.bindAttrs.find((item) => item.name === 'url');
const url = String(urlAttr?.value);
if (url) {
if (!uploaders.has(url)) {
uploaders.set(url, [nd]);
}
else {
uploaders.get(url).push(nd);
}
}
}
break;
}
case 'BindEvent':
ctx.bindEvent = nd;
break;
case 'Event':
ctx.event = nd;
break;
case 'CallLogic': {
const { thisLogic, event, bindEvent, viewLike, viewElement, viewElements } = ctx;
createArrayOnAdd(thisLogic, nd, defToCalls);
if (viewElement || bindEvent || event) {
const [kind, logicDecl] = nd.getCallNodeUsingCache(frontNdCache, serverNdCache, processNdCache, viewLike);
switch (kind) {
case 'server': {
const key = generateServiceKey(logicDecl, 'server');
createArrayOnAdd(key, { viewLike, viewElements }, backendLogicCallCtx);
break;
}
case 'process': {
const key = generateServiceKey(logicDecl, 'process');
createArrayOnAdd(key, { viewLike, viewElements }, backendLogicCallCtx);
break;
}
case 'front': {
frontendLogicCallCtx.push({ viewLike, viewElements, thisLogic: logicDecl });
break;
}
default: throw new Error('Invalid logic kind');
}
}
// 收集 callLogicUploadList
if (nd?.calleeName?.includes('downloadFile') && nd?.calleeNamespace === 'nasl.io' && !callLogicUploadFlag) {
// ViewElement 权限优先
if (viewElement instanceof concepts_1.ViewElement && viewElement?.auth) {
callLogicUploadList.set(viewElement?.authPath, [{
type: 'component',
path: viewElement?.authPath
}]);
}
else if (viewLike instanceof concepts_1.View && viewLike?.auth) {
callLogicUploadList.set(viewLike?.authPath, [{
type: 'page',
path: viewLike?.authPath
}]);
}
else {
// 如果开启权限和未开启权限都有,以未开启权限为准,清空 map,设置为 [[]]
callLogicUploadFlag = true;
callLogicUploadList.clear();
}
}
break;
}
case 'BindAttribute': {
if (nd.name === 'dataSource'
&& nd.type === 'dynamic'
&& nd.expression instanceof concepts_1.Identifier
&& (ctx.viewLike) // View | BusinessComponent
) {
const viewLogics = ctx.viewLike.logics; // Array<Logic> | Array<BusinessLogic>
const identName = nd.expression.name;
const logic = viewLogics.find((viewLogic) => {
return viewLogic?.name === identName;
});
if (logic) {
const { viewElements, viewLike: view } = ctx;
frontendLogicCallCtx.push({ viewElements, viewLike: view, thisLogic: logic });
}
}
break;
}
case 'Logic':
ctx.thisLogic = nd;
break;
case 'BusinessLogic':
ctx.thisLogic = nd;
break;
case 'View':
clearCtx(ctx);
ctx.viewLike = nd;
break;
case 'Identifier': {
// 参数选了函数名。高阶函数。
if (nd?.namespace === "app.logics") {
const key = `/api/lcplogics/${nd.name}:POST`;
createArrayOnAdd(key, { viewLike: ctx.viewLike, viewElements: ctx.viewElements }, backendLogicCallCtx);
}
// 草稿区也可能有这种 concept 是 Identifier 的调用
if (ctx?.viewLike) {
const viewLogics = ctx.viewLike.logics; // Array<Logic> | Array<BusinessLogic>
const identName = nd.name;
const logic = viewLogics.find((viewLogic) => {
return viewLogic?.name === identName;
});
if (logic) {
const { viewElements, viewLike: view } = ctx;
frontendLogicCallCtx.push({ viewElements, viewLike: view, thisLogic: logic });
}
if (nd.parentNode instanceof concepts_1.BindEvent) {
// 组件逻辑:elements.xxx.logics.load;
// 组件内置逻辑,不需要处理
// 业务组件内置逻辑,即业务组件开发外部调用的逻辑
// 页面逻辑:load; 之前代码已处理,这里不需要处理
// 组件事件逻辑: elements.xxx.bindEvents.yyy.logics.load
// 页面事件逻辑:bindEvents.yyy.logics.load; 如果页面事件逻辑调用了服务端逻辑,页面内部组件一定也有权限,不需要处理
const strs = nd.namespace?.split('.') || [];
let logic;
if (strs[0] === 'elements' && (strs[2] === 'bindEvents' || strs[2] === 'logics')) {
const elementName = strs[1];
if (!ctx.name2ViewElement) {
ctx.name2ViewElement = {};
ctx.viewLike.elements.forEach((element) => {
element.traverseStrictChildren((node) => {
if (concepts_1.asserts.isViewElement(node) &&
(node.bindEvents.length > 0 || nasl_concepts_1.businessComponentTagPrefixRegex.test(node.tag))) {
ctx.name2ViewElement[node.name] = node;
}
});
});
}
if (ctx.name2ViewElement[elementName]) {
logic = nd.getRefLogic(ctx.name2ViewElement);
logic = logic?.__v_raw ?? logic;
}
}
if (logic) {
const { viewElements, viewLike: view } = ctx;
frontendLogicCallCtx.push({ viewElements, viewLike: view, thisLogic: logic });
}
}
}
break;
}
case 'BusinessComponent': {
clearCtx(ctx);
ctx.viewLike = nd;
break;
}
case 'CallConnector': {
const { thisLogic, event, bindEvent, viewLike, viewElement, viewElements } = ctx;
createArrayOnAdd(thisLogic, nd, defToCalls);
if (viewElement || bindEvent || event) {
const [kind, logicDecl] = nd.getCallNodeUsingCache(frontNdCache, serverNdCache, processNdCache, viewLike);
switch (kind) {
case 'server': {
const key = generateServiceKey(logicDecl, 'server', nd);
createArrayOnAdd(key, { viewLike, viewElements }, backendLogicCallCtx);
break;
}
case 'process': {
const key = generateServiceKey(logicDecl, 'process', nd);
createArrayOnAdd(key, { viewLike, viewElements }, backendLogicCallCtx);
break;
}
case 'front': {
frontendLogicCallCtx.push({ viewLike, viewElements, thisLogic: logicDecl });
break;
}
default: throw new Error('Invalid logic kind');
}
}
break;
}
default: break;
}
});
for (const frontendType of app.frontendTypes) {
traverseChildrenWithContext(frontendType, traverseFn);
}
frontendLogicCallCtx.forEach(({ viewLike, viewElements, thisLogic }) => {
const visitedCalls = new Map(); // 防止 A 调用 A 自我调用或 A 调用 B,B 又调用 A 等循环调用
const findCallBackendLogic = ([kind, l], callLogic) => {
if (!l) {
return;
}
switch (kind) {
case 'server': {
const key = generateServiceKey(l, 'server', callLogic);
createArrayOnAdd(key, { viewLike, viewElements }, backendLogicCallCtx);
break;
}
case 'process': {
const key = generateServiceKey(l, 'process');
createArrayOnAdd(key, { viewLike, viewElements }, backendLogicCallCtx);
break;
}
case 'front': {
const lgcCalls = defToCalls.get(l);
lgcCalls?.forEach(call => {
if (!visitedCalls.get(l)?.has(call)) {
createSetOnAdd(l, call, visitedCalls);
if (concepts_1.asserts.isCallConnector(call)) {
findCallBackendLogic(call.getCallNodeUsingCache(frontNdCache, serverNdCache, processNdCache, viewLike), call);
}
else {
findCallBackendLogic(call.getCallNodeUsingCache(frontNdCache, serverNdCache, processNdCache, viewLike));
}
}
});
break;
}
default: {
throw new Error('Invalid logic kind in frontendLogicCallCtx');
}
}
};
findCallBackendLogic(['front', thisLogic]);
});
const logicPageResourceDtoList = {};
backendLogicCallCtx.forEach((ctx, key) => {
const resources = [];
ctx?.forEach(({ viewElements, viewLike }) => {
const pathInfos = [];
viewElements?.forEach((viewElement) => {
if (viewElement?.auth) {
pathInfos.push({
path: viewElement?.authPath,
type: 'component',
});
}
});
let viewIter = viewLike;
while (viewIter) {
if (viewIter instanceof concepts_1.View && viewIter.auth) {
pathInfos.push({
path: viewIter.authPath,
type: 'page',
});
}
viewIter = viewIter.parentNode;
}
resources.push(pathInfos);
});
logicPageResourceDtoList[key] = optimizeResourceData(resources);
});
const authVals = checkUploadAuth(uploaders);
if (authVals?.length) {
authVals.forEach((item) => {
const { key, authValue } = item;
if (key.startsWith('/upload') || key.startsWith('/v1/upload') || key.startsWith('/api')) {
logicPageResourceDtoList[`${key}:POST`] =
checkPageAndUploadAuth(uploaders, key) ? [[]] : convertArray(authValue);
}
});
}
logicPageResourceDtoList['/upload/download_files:POST'] =
callLogicUploadList.size === 0 ? [[]] : Array.from(callLogicUploadList.values());
logicPageResourceDtoList['/api/logics/downloadFile:POST'] = [[]];
logicPageResourceDtoList['/api/logics/downloadFile:GET'] = [[]];
return logicPageResourceDtoList;
}
exports.genPermissionData = genPermissionData;
// 并不通用,不需要作为成员方法放在 BaseNode.ts
function traverseChildrenWithContext(nd, cb) {
function traverse(node, context) {
cb(node, context);
const concept = node.concept;
const { childProperties, childrenProperties, } = (0, concepts_1.getConceptMeta)(concept);
for (const key of childProperties) {
const child = node[key];
if (child && child.concept.length) {
traverse(child, { ...context });
}
}
for (const key of childrenProperties) {
const children = node[key];
for (let i = 0; i < children?.length; i++) {
const item = children[i];
if (item && item.concept.length) {
traverse(item, { ...context });
}
}
}
}
// @ts-expect-error
traverse(nd.__v_raw ?? nd, {});
}
function genLogicAuthFlag(__app) {
// @ts-expect-error
const app = __app.__v_raw ?? __app;
let flag = false;
app.traverseStrictChildrenStopWhen((node) => {
if ((node instanceof concepts_1.View || node instanceof concepts_1.ViewElement) && node.auth)
flag = true;
}, (nd) => (nd instanceof concepts_1.View || nd instanceof concepts_1.ViewElement) && Boolean(nd.auth), []);
return flag;
}
exports.genLogicAuthFlag = genLogicAuthFlag;
function compareResourceNode(a, b) {
if (a.path < b.path) {
return -1;
}
if (a.path > b.path) {
return 1;
}
if (a.type < b.type) {
return -1;
}
if (a.type > b.type) {
return 1;
}
return 0;
}
function compareResourceNodeList(a, b) {
a.sort(compareResourceNode);
b.sort(compareResourceNode);
for (let i = 0; i < a.length; i++) {
const result = compareResourceNode(a[i], b[i]);
if (result !== 0) {
return result;
}
}
return 0;
}
function sortResourceDtoMap(m) {
const keys = Array.from(m.keys()).sort();
const sortedMap = new Map();
keys.forEach((key) => {
sortedMap.set(key, m.get(key).sort(compareResourceNodeList));
});
keys.forEach((key) => {
sortedMap.get(key).forEach((item) => {
item.sort(compareResourceNode);
});
});
return sortedMap;
}
exports.sortResourceDtoMap = sortResourceDtoMap;
async function testWithOldPermissionResult(app, newRes) {
const newResMap = sortResourceDtoMap(new Map(Object.entries(newRes)));
console.log('newResMap', newResMap);
const oldRes = await genPermissionDataOld(app);
const oldResMap = sortResourceDtoMap(new Map(Object.entries(oldRes)));
console.log('oldResMap', oldResMap);
console.log("oldLogicPageResourceDtoListMap === newLogicPageResourceDtoListMap?", JSON.stringify(Array.from(newResMap.entries())) === JSON.stringify(Array.from(oldResMap.entries())));
}
exports.testWithOldPermissionResult = testWithOldPermissionResult;
//# sourceMappingURL=permission.js.map