diff --git a/packages/devops-server/README.md b/packages/devops-server/README.md index e28af95d88..e23d10b53f 100644 --- a/packages/devops-server/README.md +++ b/packages/devops-server/README.md @@ -56,19 +56,5 @@ npm start ## TODO -- 【已完成】 部署数据访问策略:写入 app db __deployed__rules, app server 应监听该库之变化(watch) -- 【已完成】 部署云函数:写入 app db __deployed__functions, app-server 运行前直接读取即可 -- 【已完成】调试云函数:调用 app server 提供的调试接口,由 devops server 转发,或者发调试令牌直接调 -- 【已完成】 部署应用触发器(新增、修改),应监听该库之变化 - -- 【已完成】考虑使用 mongo watch() 替代 less-api accessor 的数据事件,应用于部署监听和云函数事件(可获取变更数据的完整信息) -- 【已完成】数据管理-集合管理:使用 devops server dbm entry,可具备完整的 app db 管理能力 -- 【已完成】将 devops 中表名修改,增加前缀, 如: __admins,以适应用户可能用同一数据库,跑 app & devops server; - -- 实现远程部署推送:远程推送源管理,推送云函数(及触发器),推送访问规则 -- 远程部署请求管理:查询收到的部署请求,可拒绝,可接受 - -- 【已完成】考虑以后去除 app server 中的 RBAC admin 相关的代码,转由云函数实现,云函数可初始配置 应用的 $injections getter -- 【已完成】将 app server 中的 admin entry 移至内置云函数中实现【使用了通用 proxy/entry 实现】 -- 进行 proxy/entry 访问测试、injector 测试和配置交互 -- 测试预置的几个新云函数 +- 实现基于 GridFS 的存储器,将内置文件存储器改为 GridFS +- 实现配置额外 npm 包功能 \ No newline at end of file diff --git a/packages/devops-server/init/index.js b/packages/devops-server/init/index.js index 776c490e37..18ac1e770e 100644 --- a/packages/devops-server/init/index.js +++ b/packages/devops-server/init/index.js @@ -64,18 +64,18 @@ async function createFirstAdmin() { const username = Config.SYS_ADMIN const password = hashPassword(Config.SYS_ADMIN_PASSWORD) - const { total } = await db.collection('__admins').count() + const { total } = await db.collection(Constants.cn.admins).count() if (total > 0) { console.log('admin already exists') return } - await sys_accessor.db.collection('__admins').createIndex('username', { unique: true }) + await sys_accessor.db.collection(Constants.cn.admins).createIndex('username', { unique: true }) - const { data } = await db.collection('__roles').get() + const { data } = await db.collection(Constants.cn.roles).get() const roles = data.map(it => it.name) - const r_add = await db.collection('__admins').add({ + const r_add = await db.collection(Constants.cn.admins).add({ username, avatar: "https://static.dingtalk.com/media/lALPDe7szaMXyv3NAr3NApw_668_701.png", name: 'Admin', @@ -85,7 +85,7 @@ async function createFirstAdmin() { }) assert(r_add.ok, 'add admin occurs error') - await db.collection('__password').add({ + await db.collection(Constants.cn.password).add({ uid: r_add.id, password, type: 'login', @@ -106,14 +106,14 @@ async function createFirstAdmin() { async function createFirstRole() { try { - await sys_accessor.db.collection('__roles').createIndex('name', { unique: true }) + await sys_accessor.db.collection(Constants.cn.roles).createIndex('name', { unique: true }) - const r_perm = await db.collection('__permissions').get() + const r_perm = await db.collection(Constants.cn.permissions).get() assert(r_perm.ok, 'get permissions failed') const permissions = r_perm.data.map(it => it.name) - const r_add = await db.collection('__roles').add({ + const r_add = await db.collection(Constants.cn.roles).add({ name: 'superadmin', label: '超级管理员', description: '系统初始化的超级管理员', @@ -141,7 +141,7 @@ async function createFirstRole() { async function createInitialPermissions() { // 创建唯一索引 - await sys_accessor.db.collection('__permissions').createIndex('name', { unique: true }) + await sys_accessor.db.collection(Constants.cn.permissions).createIndex('name', { unique: true }) for (const perm of permissions) { try { @@ -150,7 +150,7 @@ async function createInitialPermissions() { created_at: Date.now(), updated_at: Date.now() } - await db.collection('__permissions').add(data) + await db.collection(Constants.cn.permissions).add(data) console.log('permissions added: ' + perm.name) } catch (error) { @@ -175,7 +175,7 @@ async function createInitialPermissions() { async function createInitialPolicy(name, rules, injector) { // if policy existed, skip it - const { total } = await db.collection('__policies') + const { total } = await db.collection(Constants.cn.policies) .where({ name: name }) .count() @@ -184,10 +184,10 @@ async function createInitialPolicy(name, rules, injector) { return } - await sys_accessor.db.collection('__policies').createIndex('name', { unique: true }) + await sys_accessor.db.collection(Constants.cn.policies).createIndex('name', { unique: true }) // add policy - await db.collection('__policies').add({ + await db.collection(Constants.cn.policies).add({ name: name, rules: rules, status: 1, @@ -205,7 +205,7 @@ async function createInitialPolicy(name, rules, injector) { */ async function createBuiltinFunctions() { // 创建云函数索引 - await sys_accessor.db.collection('__functions').createIndex('name', { unique: true }) + await sys_accessor.db.collection(Constants.cn.functions).createIndex('name', { unique: true }) const loader = new FunctionLoader() @@ -219,7 +219,7 @@ async function createBuiltinFunctions() { updated_at: Date.now() } delete data['triggers'] - const r = await db.collection('__functions').add(data) + const r = await db.collection(Constants.cn.functions).add(data) if (triggers.length) { await createTriggers(r.id, triggers) @@ -250,7 +250,7 @@ async function createTriggers(func_id, triggers) { updated_at: Date.now(), func_id: func_id } - await db.collection('__triggers').add(data) + await db.collection(Constants.cn.triggers).add(data) } console.log(`triggers of func[${func_id}] created`) diff --git a/packages/devops-server/src/api/function-log.ts b/packages/devops-server/src/api/function-log.ts deleted file mode 100644 index b922f24724..0000000000 --- a/packages/devops-server/src/api/function-log.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Globals } from "../lib/globals" - -const db = Globals.sys_db - -/** - * 添加函数执行日志 - * @param data - * @returns - */ -export async function addFunctionLog(data: any) { - if(!data) return null - - const r = await db.collection('__function_logs') - .add(data) - - return r.id -} \ No newline at end of file diff --git a/packages/devops-server/src/api/function.ts b/packages/devops-server/src/api/function.ts index 3040ccd11c..d15576803d 100644 --- a/packages/devops-server/src/api/function.ts +++ b/packages/devops-server/src/api/function.ts @@ -13,7 +13,7 @@ const db = Globals.sys_db * @returns */ export async function getFunctionByName(func_name: string) { - const r = await db.collection('__functions') + const r = await db.collection(Constants.cn.functions) .where({ name: func_name }) .getOne() @@ -31,7 +31,7 @@ export async function getFunctionByName(func_name: string) { */ export async function getFunctionById(func_id: string) { // 获取函数 - const r = await db.collection('__functions') + const r = await db.collection(Constants.cn.functions) .where({ _id: func_id }) .getOne() @@ -46,13 +46,13 @@ export async function getFunctionById(func_id: string) { /** * 发布云函数 - * 实为将 sys db __functions 集合,复制其数据至 app db 中 + * 实为将 sys db functions 集合,复制其数据至 app db 中 */ export async function publishFunctions() { const logger = Globals.logger const app_accessor = Globals.app_accessor - const ret = await Globals.sys_accessor.db.collection('__functions').find().toArray() + const ret = await Globals.sys_accessor.db.collection(Constants.cn.functions).find().toArray() // compile const data = ret.map(fn => compileFunction(fn)) @@ -115,7 +115,7 @@ async function _deployOneFunction(func: CloudFunctionStruct, session: ClientSess await _deleteFunctionWithSameNameButNotId(func, session) const db = Globals.sys_accessor.db - const r = await db.collection('__functions').findOne({ _id: new ObjectId(func._id) }, { session }) + const r = await db.collection(Constants.cn.functions).findOne({ _id: new ObjectId(func._id) }, { session }) const data = { ...func @@ -124,7 +124,7 @@ async function _deployOneFunction(func: CloudFunctionStruct, session: ClientSess // if exists function if (r) { delete data['_id'] - const ret = await db.collection('__functions').updateOne({ _id: r._id }, { + const ret = await db.collection(Constants.cn.functions).updateOne({ _id: r._id }, { $set: data }, { session }) @@ -135,7 +135,7 @@ async function _deployOneFunction(func: CloudFunctionStruct, session: ClientSess // if new function data._id = new ObjectId(data._id) as any - const ret = await db.collection('__functions').insertOne(data as any, { session }) + const ret = await db.collection(Constants.cn.functions).insertOne(data as any, { session }) assert(ret.insertedId, `deploy: add function ${func.name} occurred error`) } @@ -145,7 +145,7 @@ async function _deployOneFunction(func: CloudFunctionStruct, session: ClientSess */ async function _deleteFunctionWithSameNameButNotId(func: CloudFunctionStruct, session: ClientSession) { const db = Globals.sys_accessor.db - await db.collection('__functions').findOneAndDelete({ + await db.collection(Constants.cn.functions).findOneAndDelete({ _id: { $ne: new ObjectId(func._id) }, diff --git a/packages/devops-server/src/api/permission.ts b/packages/devops-server/src/api/permission.ts index b2dca0aa89..b11b26fcf5 100644 --- a/packages/devops-server/src/api/permission.ts +++ b/packages/devops-server/src/api/permission.ts @@ -1,5 +1,6 @@ import * as assert from 'assert' +import { Constants } from '../constants' import { Globals } from '../lib/globals' const db = Globals.sys_db @@ -30,14 +31,14 @@ export async function checkPermission(uid: string, permission: string): Promise< export async function getPermissions(uid: string) { // 查用户 - const { data: admin } = await db.collection('__admins') + const { data: admin } = await db.collection(Constants.cn.admins) .where({ _id: uid }) .getOne() assert(admin, 'getPermissions failed') // 查角色 - const { data: roles } = await db.collection('__roles') + const { data: roles } = await db.collection(Constants.cn.roles) .where({ name: { $in: admin.roles ?? [] diff --git a/packages/devops-server/src/api/rules.ts b/packages/devops-server/src/api/rules.ts index 3bb8804397..1884c2c8ae 100644 --- a/packages/devops-server/src/api/rules.ts +++ b/packages/devops-server/src/api/rules.ts @@ -3,45 +3,16 @@ import { Constants } from '../constants' import { Globals } from "../lib/globals" import { ClientSession, ObjectId } from 'mongodb' -const db = Globals.sys_db -export interface RuleDocument { - category: string, - collection: string, - data: Object -} - -/** - * 根据类别获取策略规则 - * @param category 策略类别 - * @returns - */ -export async function getAccessPolicy(category: string): Promise { - const r = await db.collection('__rules') - .where({ category }) - .get() - - assert.ok(r.ok && r.data.length, `read rules failed: ${category}`) - - const rules = r.data - - const ruleMap = {} - for (const rule of rules) { - const key = rule['collection'] - ruleMap[key] = JSON.parse(rule['data']) - } - - return ruleMap -} /** * 发布访问策略 - * 实为将 sys_db.__rules 中的表,复制其数据至 app_db 中 + * 实为将 sys_db policies 中的文档,复制其数据至 app_db 中 */ export async function publishAccessPolicy() { const logger = Globals.logger const app_accessor = Globals.app_accessor - const ret = await Globals.sys_accessor.db.collection('__policies').find().toArray() + const ret = await Globals.sys_accessor.db.collection(Constants.cn.policies).find().toArray() const session = app_accessor.conn.startSession() try { @@ -92,7 +63,7 @@ async function _deployOnePolicy(policy: any, session: ClientSession) { await _deletePolicyWithSameNameButNotId(policy, session) const db = Globals.sys_accessor.db - const r = await db.collection('__policies').findOne({ _id: new ObjectId(policy._id) }, { session }) + const r = await db.collection(Constants.cn.policies).findOne({ _id: new ObjectId(policy._id) }, { session }) const data = { ...policy @@ -102,7 +73,7 @@ async function _deployOnePolicy(policy: any, session: ClientSession) { // if exists if (r) { delete data['_id'] - const ret = await db.collection('__policies').updateOne({ _id: r._id }, { + const ret = await db.collection(Constants.cn.policies).updateOne({ _id: r._id }, { $set: data }, { session }) @@ -112,7 +83,7 @@ async function _deployOnePolicy(policy: any, session: ClientSession) { // if new data._id = new ObjectId(data._id) as any - const ret = await db.collection('__policies').insertOne(data as any, { session }) + const ret = await db.collection(Constants.cn.policies).insertOne(data as any, { session }) assert(ret.insertedId, `deploy: add policy ${policy.name} occurred error`) } @@ -122,7 +93,7 @@ async function _deployOnePolicy(policy: any, session: ClientSession) { */ async function _deletePolicyWithSameNameButNotId(policy: any, session: ClientSession) { const db = Globals.sys_accessor.db - await db.collection('__policies').findOneAndDelete({ + await db.collection(Constants.cn.policies).findOneAndDelete({ _id: { $ne: new ObjectId(policy._id) }, diff --git a/packages/devops-server/src/api/trigger.ts b/packages/devops-server/src/api/trigger.ts index 79fb4aa007..2f5cf4fe3a 100644 --- a/packages/devops-server/src/api/trigger.ts +++ b/packages/devops-server/src/api/trigger.ts @@ -13,7 +13,7 @@ const logger = Globals.logger * @returns */ export async function getTriggers(status = 1) { - const r = await db.collection('__triggers') + const r = await db.collection(Constants.cn.triggers) .where({ status: status }) .get() @@ -26,7 +26,7 @@ export async function getTriggers(status = 1) { * @returns */ export async function getTriggerById(id: string) { - const r = await db.collection('__triggers') + const r = await db.collection(Constants.cn.triggers) .where({ _id: id }) .getOne() @@ -36,13 +36,13 @@ export async function getTriggerById(id: string) { /** * 发布触发器 - * 实为将 sys db __triggers 集合,复制其数据至 app db 中 + * 实为将 sys db triggers 集合,复制其数据至 app db 中 */ export async function publishTriggers() { const logger = Globals.logger const app_accessor = Globals.app_accessor - const ret = await Globals.sys_accessor.db.collection('__triggers').find().toArray() + const ret = await Globals.sys_accessor.db.collection(Constants.cn.triggers).find().toArray() const session = app_accessor.conn.startSession() try { @@ -91,7 +91,7 @@ export async function deployTriggers(triggers: any[]) { async function _deployOneTrigger(trigger: any, session: ClientSession) { const db = Globals.sys_accessor.db - const r = await db.collection('__triggers').findOne({ _id: new ObjectId(trigger._id) }, { session }) + const r = await db.collection(Constants.cn.triggers).findOne({ _id: new ObjectId(trigger._id) }, { session }) const data = { ...trigger @@ -101,7 +101,7 @@ async function _deployOneTrigger(trigger: any, session: ClientSession) { // if exists function if (r) { delete data['_id'] - const ret = await db.collection('__triggers').updateOne({ _id: r._id }, { + const ret = await db.collection(Constants.cn.triggers).updateOne({ _id: r._id }, { $set: data }, { session }) @@ -111,6 +111,6 @@ async function _deployOneTrigger(trigger: any, session: ClientSession) { // if new function data._id = new ObjectId(data._id) as any - const ret = await db.collection('__triggers').insertOne(data as any, { session }) + const ret = await db.collection(Constants.cn.triggers).insertOne(data as any, { session }) assert(ret.insertedId, `deploy: add trigger ${trigger.name} occurred error`) } \ No newline at end of file diff --git a/packages/devops-server/src/constants.ts b/packages/devops-server/src/constants.ts index c99d2456cb..cc45e0d250 100644 --- a/packages/devops-server/src/constants.ts +++ b/packages/devops-server/src/constants.ts @@ -1,5 +1,6 @@ import { deepFreeze } from "./lib/utils/lang" +const coll_prefix = 'devops_' export const Constants = { /** @@ -16,6 +17,27 @@ export const Constants = { * 部署到 app db 中的访问策略集合名 */ policy_collection: '__deployed__policies', + + /** + * prefix of sys db collection name + */ + coll_prefix: coll_prefix, + + /** + * sys db collection names + */ + cn: { + admins: coll_prefix + 'admins', + permissions: coll_prefix + 'permissions', + roles: coll_prefix + 'roles', + policies: coll_prefix + 'policies', + functions: coll_prefix + 'functions', + function_history: coll_prefix + 'function_history', + triggers: coll_prefix + 'triggers', + deploy_targets: coll_prefix + 'deploy_targets', + deploy_requests: coll_prefix + 'deploy_requests', + password: coll_prefix + 'password' + } } deepFreeze(Constants) \ No newline at end of file diff --git a/packages/devops-server/src/router/admin/handlers.ts b/packages/devops-server/src/router/admin/handlers.ts index c9501ffaa0..d89fd5129f 100644 --- a/packages/devops-server/src/router/admin/handlers.ts +++ b/packages/devops-server/src/router/admin/handlers.ts @@ -4,6 +4,7 @@ import { checkPermission, getPermissions } from '../../api/permission' import { hashPassword } from '../../lib/utils/hash' import { Globals } from '../../lib/globals' import Config from '../../config' +import { Constants } from '../../constants' const db = Globals.sys_db const logger = Globals.logger @@ -23,10 +24,10 @@ export async function handleAdminLogin(req: Request, res: Response) { }) } - const ret = await db.collection('__admins') + const ret = await db.collection(Constants.cn.admins) .withOne({ query: db - .collection('__password') + .collection(Constants.cn.password) .where({ password: hashPassword(password), type: 'login' }), localField: '_id', foreignField: 'uid' @@ -87,7 +88,7 @@ export async function handleAdminInfo(req: Request, res: Response) { } // - const ret = await db.collection('__admins') + const ret = await db.collection(Constants.cn.admins) .where({ _id: uid }) .get() @@ -133,7 +134,7 @@ export async function handleAdminAdd(req: Request, res: Response) { } // 验证用户是否已存在 - const { total } = await db.collection('__admins').where({ username }).count() + const { total } = await db.collection(Constants.cn.admins).where({ username }).count() if (total > 0) { return res.send({ code: 1, @@ -142,7 +143,7 @@ export async function handleAdminAdd(req: Request, res: Response) { } // 验证 roles 是否合法 - const { total: valid_count } = await db.collection('__roles') + const { total: valid_count } = await db.collection(Constants.cn.roles) .where({ name: db.command.in(roles) }).count() @@ -155,7 +156,7 @@ export async function handleAdminAdd(req: Request, res: Response) { } // add admin - const r = await db.collection('__admins') + const r = await db.collection(Constants.cn.admins) .add({ username, name: name ?? null, @@ -166,7 +167,7 @@ export async function handleAdminAdd(req: Request, res: Response) { }) // add admin password - await db.collection('__password') + await db.collection(Constants.cn.password) .add({ uid: r.id, password: hashPassword(password), @@ -208,7 +209,7 @@ export async function handleAdminEdit(req: Request, res: Response) { } // 验证 uid 是否合法 - const { data: admins } = await db.collection('__admins').where({ _id: uid }).get() + const { data: admins } = await db.collection(Constants.cn.admins).where({ _id: uid }).get() if (!admins || !admins.length) { return res.send({ code: 1, @@ -217,7 +218,7 @@ export async function handleAdminEdit(req: Request, res: Response) { } // 验证 roles 是否合法 - const { total: valid_count } = await db.collection('__roles') + const { total: valid_count } = await db.collection(Constants.cn.roles) .where({ name: db.command.in(roles) }).count() @@ -231,7 +232,7 @@ export async function handleAdminEdit(req: Request, res: Response) { // update password if (password) { - await db.collection('__password') + await db.collection(Constants.cn.password) .where({ uid: uid }) .update({ password: hashPassword(password), @@ -248,7 +249,7 @@ export async function handleAdminEdit(req: Request, res: Response) { // username if (username && username != old.username) { - const { total } = await db.collection('__admins').where({ username }).count() + const { total } = await db.collection(Constants.cn.admins).where({ username }).count() if (total) { return res.send({ code: 1, @@ -273,7 +274,7 @@ export async function handleAdminEdit(req: Request, res: Response) { data['roles'] = roles } - const r = await db.collection('__admins') + const r = await db.collection(Constants.cn.admins) .where({ _id: uid }) .update(data) diff --git a/packages/devops-server/src/router/deploy/index.ts b/packages/devops-server/src/router/deploy/index.ts index e0247f1bd5..c1ccf465b2 100644 --- a/packages/devops-server/src/router/deploy/index.ts +++ b/packages/devops-server/src/router/deploy/index.ts @@ -6,6 +6,7 @@ import { Globals } from '../../lib/globals' import { getToken, parseToken } from '../../lib/utils/token' import { deployPolicies, publishAccessPolicy } from '../../api/rules' import { deployTriggers, publishTriggers } from '../../api/trigger' +import { Constants } from '../../constants' export const DeployRouter = express.Router() const logger = Globals.logger @@ -106,7 +107,7 @@ DeployRouter.post('/in', async (req, res) => { created_at: Date.now() } - await db.collection('deploy_requests').add(data) + await db.collection(Constants.cn.deploy_requests).add(data) } // 入库云函数 @@ -121,7 +122,7 @@ DeployRouter.post('/in', async (req, res) => { created_at: Date.now() } - await db.collection('deploy_requests').add(data) + await db.collection(Constants.cn.deploy_requests).add(data) } return res.send({ @@ -151,7 +152,7 @@ DeployRouter.post('/apply', async (req, res) => { } const db = Globals.sys_db - const r = await db.collection('deploy_requests').where({ _id: id }).getOne() + const r = await db.collection(Constants.cn.deploy_requests).where({ _id: id }).getOne() if (!r.ok || !r.data) { return res.status(404).send('deploy request not found') } @@ -180,7 +181,7 @@ DeployRouter.post('/apply', async (req, res) => { } // update deploy request status to 'deployed' - await db.collection('deploy_requests').where({ _id: id }).update({ status: 'deployed' }) + await db.collection(Constants.cn.deploy_requests).where({ _id: id }).update({ status: 'deployed' }) return res.send({ code: 0, diff --git a/packages/devops-server/src/router/sys_rules.ts b/packages/devops-server/src/router/sys_rules.ts index fbe6c2c900..ce8b0be92e 100644 --- a/packages/devops-server/src/router/sys_rules.ts +++ b/packages/devops-server/src/router/sys_rules.ts @@ -1,16 +1,20 @@ +import { Constants } from "../constants" + +const cn = Constants.cn + export default { - "__admins": { + [cn.admins]: { "read": "$has('admin.read')", "update": "$has('admin.edit')", "add": "$has('admin.create')", "remove": "$has('admin.delete')", "count": "$has('admin.read')" }, - "__permissions": { + [cn.permissions]: { "read": "$has('permission.read')", "count": "$has('permission.read')" }, - "__roles": { + [cn.roles]: { "read": "$has('role.read')", "update": "$has('role.edit')", "add": "$has('role.create')", @@ -19,20 +23,20 @@ export default { "query": { "name": { "required": true, - "notExists": "/__admins/roles" + "notExists": `/${cn.admins}/roles` } } }, "count": "$has('role.read')" }, - "__policies": { + [cn.policies]: { "read": "$has('policy.read')", "update": "$has('policy.edit')", "add": "$has('policy.create')", "remove": "$has('policy.delete')", "count": "$has('policy.read')" }, - "__functions": { + [cn.functions]: { "read": "$has('function.read')", "update": "$has('function.edit')", "add": "$has('function.create')", @@ -40,7 +44,7 @@ export default { "condition": "$has('function.delete')", "query": { "_id": { - "notExists": "/triggers/func_id" + "notExists": `/${cn.triggers}/func_id` }, "status": { "required": true, @@ -54,31 +58,26 @@ export default { }, "count": "$has('function.read')" }, - "__function_logs": { - "read": "$has('function_logs.read')", - "remove": "$has('function_logs.remove')", - "count": "$has('function_logs.read')" - }, - "__function_history": { + [cn.function_history]: { "read": "$has('function_history.read')", "add": "$has('function_history.create')", "count": "$has('function_history.read')" }, - "__triggers": { + [cn.triggers]: { "read": "$has('trigger.read')", "update": "$has('trigger.edit')", "add": "$has('trigger.create')", "remove": "$has('trigger.delete') && query.status === 0", "count": "$has('trigger.read')" }, - "deploy_targets": { + [cn.deploy_targets]: { "read": "$has('deploy_target.read')", "update": "$has('deploy_target.edit')", "add": "$has('deploy_target.create')", "remove": "$has('deploy_target.delete')", "count": "$has('deploy_target.read')" }, - "deploy_requests": { + [cn.deploy_requests]: { "read": "$has('deploy_request.read')", "update": "$has('deploy_request.edit')", "add": "$has('deploy_request.create')",