From 446740f742ea519614cb78b7707edd57abc171c8 Mon Sep 17 00:00:00 2001 From: maslow Date: Mon, 30 Aug 2021 18:29:25 +0800 Subject: [PATCH] feat(system-server): refactor system server apis, support appid; --- packages/system-server/src/api/function.ts | 189 +++++++++++++++++++ packages/system-server/src/api/permission.ts | 54 ++++++ packages/system-server/src/api/policy.ts | 120 ++++++++++++ packages/system-server/src/api/trigger.ts | 139 ++++++++++++++ 4 files changed, 502 insertions(+) create mode 100644 packages/system-server/src/api/function.ts create mode 100644 packages/system-server/src/api/permission.ts create mode 100644 packages/system-server/src/api/policy.ts create mode 100644 packages/system-server/src/api/trigger.ts diff --git a/packages/system-server/src/api/function.ts b/packages/system-server/src/api/function.ts new file mode 100644 index 0000000000..9f8946f02b --- /dev/null +++ b/packages/system-server/src/api/function.ts @@ -0,0 +1,189 @@ +/* + * @Author: Maslow + * @Date: 2021-07-30 10:30:29 + * @LastEditTime: 2021-08-29 18:23:38 + * @Description: + */ + +import { Constants } from "../constants" +import { DatabaseAgent } from "../lib/db-agent" +import { compileTs2js } from 'cloud-function-engine/dist/utils' +import { CloudFunctionStruct } from "cloud-function-engine" +import { ClientSession, ObjectId } from 'mongodb' +import * as assert from 'assert' +import { logger } from "../lib/logger" +import { ApplicationStruct, getApplicationDbAccessor } from "./application" + +const db = DatabaseAgent.sys_db + +/** + * Load function data by its name + * @param func_name + * @returns + */ +export async function getFunctionByName(appid: string, func_name: string) { + const r = await db.collection(Constants.cn.functions) + .where({ name: func_name, appid }) + .getOne() + + assert.ok(r.ok, `getCloudFunction() failed to get function [${func_name}]: ${r.error.toString()}`) + return r.data +} + +/** + * Load function data by its id + * @param func_name + * @returns + */ +export async function getFunctionById(appid: string, func_id: string) { + const r = await db.collection(Constants.cn.functions) + .where({ _id: func_id, appid }) + .getOne() + + assert.ok(r.ok, `getCloudFunctionById() failed to get function [${func_id}]: ${r.error.toString()}`) + return r.data +} + + + +/** + * Publish functions + * Means that copying sys db functions to app db + */ +export async function publishFunctions(app: ApplicationStruct) { + + // read functions from sys db + const ret = await DatabaseAgent.sys_accessor.db + .collection(Constants.cn.functions) + .find({ + appid: app._id + }) + .toArray() + + // compile functions + const data = ret.map(fn => compileFunction(fn)) + + // write functions to app db + const app_accessor = await getApplicationDbAccessor(app) + const session = app_accessor.conn.startSession() + + try { + await session.withTransaction(async () => { + const _db = app_accessor.db + const app_coll = _db.collection(Constants.function_collection) + await app_coll.deleteMany({}, { session }) + await app_coll.insertMany(data, { session }) + }) + } catch (error) { + logger.error(error) + } finally { + await session.endSession() + } +} + +/** + * Compile function codes (from typescript to javascript) + * @param func + */ +function compileFunction(func: any) { + func.compiledCode = compileTs2js(func.code) + return func +} + +/** + * Deploy functions which pushed from remote environment + */ +export async function deployFunctions(functions: CloudFunctionStruct[]) { + assert.ok(functions) + assert.ok(functions instanceof Array) + + const accessor = DatabaseAgent.sys_accessor + + const data = functions + const session = accessor.conn.startSession() + + try { + await session.withTransaction(async () => { + for (const func of data) { + await _deployOneFunction(func, session) + } + }) + } finally { + await session.endSession() + } +} + +/** + * Deploy a function, use in `deployFunctions()` + * @param func the cloud function to be deployed + * @param session mongodb session for transaction operations + * @private + * @see deployFunctions() + * @returns + */ +async function _deployOneFunction(func: CloudFunctionStruct, session: ClientSession) { + + await _processFunctionWithSameNameButNotId(func, session) + + const db = DatabaseAgent.sys_accessor.db + const r = await db.collection(Constants.cn.functions).findOne({ _id: new ObjectId(func._id) }, { session }) + + const data = { + ...func + } + + // if exists function + if (r) { + delete data['_id'] + const ret = await db.collection(Constants.cn.functions).updateOne({ _id: r._id }, { + $set: data + }, { session }) + + assert(ret.matchedCount, `deploy: update function ${func.name} occurred error`) + return + } + + // if new function + data._id = new ObjectId(data._id) as any + + const ret = await db.collection(Constants.cn.functions).insertOne(data as any, { session }) + assert(ret.insertedId, `deploy: add function ${func.name} occurred error`) +} + +/** + * Remove functions which have same name but different _id. + * @param func the function to be processing + * @param session the mongodb session for transaction operations + */ +async function _processFunctionWithSameNameButNotId(func: CloudFunctionStruct, session: ClientSession) { + const db = DatabaseAgent.sys_accessor.db + + // remove functions + const r = await db.collection(Constants.cn.functions).findOneAndDelete( + { + _id: { + $ne: new ObjectId(func._id) + }, + name: func.name + }, + { session }) + + if (!r.value) { + return + } + + logger.warn(`delete local func ${r?.value?._id} with same name with (${func._id}:${func.name}) but different id `) + + // remove its triggers if any + const r0 = await db.collection(Constants.cn.triggers).deleteMany( + { + func_id: r?.value?._id?.toString() + }, + { session }) + + if (!r0.deletedCount) { + return + } + + logger.warn(`delete triggers of func ${func._id} which been deleted`, r0) +} \ No newline at end of file diff --git a/packages/system-server/src/api/permission.ts b/packages/system-server/src/api/permission.ts new file mode 100644 index 0000000000..89346875e7 --- /dev/null +++ b/packages/system-server/src/api/permission.ts @@ -0,0 +1,54 @@ +/* + * @Author: Maslow + * @Date: 2021-07-30 10:30:29 + * @LastEditTime: 2021-08-30 15:48:51 + * @Description: + */ + +import * as assert from 'assert' +import { Constants } from '../constants' +import { ApplicationStruct } from './application' + +/** + * Check if a user have permission for application + * @param uid the account id + * @param permission the permission name + * @param app the application which checked for + * @returns 0 means ok, 401 means unauthorized, 403 means permission denied + */ +export async function checkPermission(uid: string, permission: string, app: ApplicationStruct): Promise { + if (!uid) return 401 + assert.ok(permission, 'empty permission got') + assert.ok(app, 'empty app got') + + // pass directly while the app owner here + if (uid === app.created_by) return 0 + + // reject while uid is not the collaborator + const [collaborator] = app.collaborators?.filter(co => co.uid === uid) ?? [] + if (!collaborator) return 403 + + // reject while uid not have the permission + const roles = collaborator.roles + const permissions = getPermissionsOfRoles(roles) + if (!permissions.includes(permission)) { + return 403 + } + + return 0 +} + +/** + * Get permission names by a list of role names + * @param roles_names The role names + * @returns + */ +export function getPermissionsOfRoles(roles_names: string[]) { + const permissions = [] + for (const name of roles_names) { + const pns = Constants.roles[name]?.permissions ?? [] + permissions.push(...pns) + } + + return permissions.map(pn => pn.name) +} \ No newline at end of file diff --git a/packages/system-server/src/api/policy.ts b/packages/system-server/src/api/policy.ts new file mode 100644 index 0000000000..97d1f20f4f --- /dev/null +++ b/packages/system-server/src/api/policy.ts @@ -0,0 +1,120 @@ +/* + * @Author: Maslow + * @Date: 2021-07-30 10:30:29 + * @LastEditTime: 2021-08-29 18:24:59 + * @Description: + */ + +import * as assert from 'assert' +import { Constants } from '../constants' +import { DatabaseAgent } from "../lib/db-agent" +import { ClientSession, ObjectId } from 'mongodb' +import { ApplicationStruct, getApplicationDbAccessor } from './application' + + +/** + * Publish access policies + * Means that copying sys db functions to app db + */ +export async function publishAccessPolicy(app: ApplicationStruct) { + // read policies from sys db + const ret = await DatabaseAgent.sys_accessor.db + .collection(Constants.cn.policies) + .find({ + appid: app._id + }) + .toArray() + + // write policies to app db + const app_accessor = await getApplicationDbAccessor(app) + const session = app_accessor.conn.startSession() + + try { + await session.withTransaction(async () => { + const _db = app_accessor.db + const app_coll = _db.collection(Constants.policy_collection) + await app_coll.deleteMany({}, { session }) + await app_coll.insertMany(ret, { session }) + }) + } finally { + await session.endSession() + } +} + + +/** + * Deploy policies which pushed from remote environment + */ +export async function deployPolicies(policies: any[]) { + assert.ok(policies) + assert.ok(policies instanceof Array) + + const accessor = DatabaseAgent.sys_accessor + + const data = policies + const session = accessor.conn.startSession() + + try { + await session.withTransaction(async () => { + for (const item of data) { + await _deployOnePolicy(item, session) + } + }) + } finally { + await session.endSession() + } +} + +/** + * Deploy a policy used by `deployPolicies`. + * @param policy the policy data to be deployed + * @param session the mongodb session for transaction operations + * @see deployPolicies + * @private + * @returns + */ +async function _deployOnePolicy(policy: any, session: ClientSession) { + + await _deletePolicyWithSameNameButNotId(policy, session) + + const db = DatabaseAgent.sys_accessor.db + const r = await db.collection(Constants.cn.policies).findOne({ _id: new ObjectId(policy._id) }, { session }) + + const data = { + ...policy + } + + + // if exists + if (r) { + delete data['_id'] + const ret = await db.collection(Constants.cn.policies).updateOne({ _id: r._id }, { + $set: data + }, { session }) + + assert(ret.matchedCount, `deploy: update policy ${policy.name} occurred error`) + return + } + + // if new + data._id = new ObjectId(data._id) as any + const ret = await db.collection(Constants.cn.policies).insertOne(data as any, { session }) + assert(ret.insertedId, `deploy: add policy ${policy.name} occurred error`) +} + +/** + * Remove policy which have same name but different _id. + * @param policy the policy to be processing + * @param session the mongodb session for transaction operations + * @see _deployOnePolicy() + * @private + */ +async function _deletePolicyWithSameNameButNotId(policy: any, session: ClientSession) { + const db = DatabaseAgent.sys_accessor.db + await db.collection(Constants.cn.policies).findOneAndDelete({ + _id: { + $ne: new ObjectId(policy._id) + }, + name: policy.name + }, { session }) +} \ No newline at end of file diff --git a/packages/system-server/src/api/trigger.ts b/packages/system-server/src/api/trigger.ts new file mode 100644 index 0000000000..47ff11c1c4 --- /dev/null +++ b/packages/system-server/src/api/trigger.ts @@ -0,0 +1,139 @@ +/* + * @Author: Maslow + * @Date: 2021-07-30 10:30:29 + * @LastEditTime: 2021-08-29 18:25:36 + * @Description: + */ + +import { Constants } from "../constants" +import { DatabaseAgent } from "../lib/db-agent" +import { ClientSession, ObjectId } from 'mongodb' +import * as assert from 'assert' +import { logger } from "../lib/logger" +import { ApplicationStruct, getApplicationDbAccessor } from "./application" + +const db = DatabaseAgent.sys_db + +/** + * load triggers + * @param status the status of trigger, default is 1 means enabled + * @returns + */ +export async function getTriggers(status = 1) { + const r = await db.collection(Constants.cn.triggers) + .where({ status: status }) + .get() + + return r.data +} + +/** + * load trigger by its id + * @param id + * @returns + */ +export async function getTriggerById(id: string) { + const r = await db.collection(Constants.cn.triggers) + .where({ _id: id }) + .getOne() + + return r.data +} + + +/** + * Publish triggers + * Means that copying sys db functions to app db + */ +export async function publishTriggers(app: ApplicationStruct) { + // read triggers from sys db + const ret = await DatabaseAgent.sys_accessor.db + .collection(Constants.cn.triggers) + .find({ + appid: app._id + }) + .toArray() + + // write triggers to app db + const app_accessor = await getApplicationDbAccessor(app) + const session = app_accessor.conn.startSession() + + try { + await session.withTransaction(async () => { + const _db = app_accessor.db + const app_coll = _db.collection(Constants.trigger_collection) + await app_coll.deleteMany({}, { session }) + await app_coll.insertMany(ret, { session }) + }) + } finally { + await session.endSession() + } +} + + +/** + * Deploy triggers which pushed from remote environment + */ +export async function deployTriggers(triggers: any[]) { + assert.ok(triggers) + assert.ok(triggers instanceof Array) + + const accessor = DatabaseAgent.sys_accessor + + const data = triggers + const session = accessor.conn.startSession() + + try { + await session.withTransaction(async () => { + for (const func of data) { + await _deployOneTrigger(func, session) + } + }) + } finally { + await session.endSession() + } +} + + +/** + * Deploy a trigger used by `deployTriggers`. + * @param trigger the trigger data to be deployed + * @param session the mongodb session for transaction operations + * @see deployTriggers + * @private + * @returns + */ +async function _deployOneTrigger(trigger: any, session: ClientSession) { + const db = DatabaseAgent.sys_accessor.db + + // skip trigger with invalid func_id + const func = await db.collection(Constants.cn.functions).findOne({ _id: new ObjectId(trigger.func_id) }) + if (!func) { + logger.warn(`skip trigger with invalid func_id: ${trigger.func_id}`, trigger) + return + } + + const r = await db.collection(Constants.cn.triggers).findOne({ _id: new ObjectId(trigger._id) }, { session }) + + const data = { + ...trigger + } + + logger.debug('deploy trigger: ', data, r) + + // if exists trigger + if (r) { + delete data['_id'] + const ret = await db.collection(Constants.cn.triggers).updateOne({ _id: r._id }, { + $set: data + }, { session }) + + assert(ret.matchedCount, `deploy: update trigger ${trigger.name} occurred error`) + return + } + + // if new trigger + data._id = new ObjectId(data._id) as any + 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