UNPKG

@lcap/nasl

Version:

NetEase Application Specific Language

729 lines 30.4 kB
"use strict"; 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