refactor: 数据库授权凭证迁移

This commit is contained in:
meilin.huang
2024-04-12 13:24:20 +08:00
parent 4ef8d27b1e
commit abc015aec0
47 changed files with 1011 additions and 558 deletions

View File

@@ -5,10 +5,10 @@ export const TagResourceTypeEnum = {
AuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor' }),
Db: EnumValue.of(2, '数据库').setExtra({ icon: 'Coin' }),
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'iconfont icon-redis' }),
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'iconfont icon-mongo' }),
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor' }).tagTypeSuccess(),
Db: EnumValue.of(2, '数据库').setExtra({ icon: 'Coin' }).tagTypeWarning(),
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'iconfont icon-redis' }).tagTypeInfo(),
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'iconfont icon-mongo' }).tagTypeDanger(),
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket' }),
};

View File

@@ -4,6 +4,6 @@ export const AccountUsernamePattern = {
};
export const ResourceCodePattern = {
pattern: /^[a-zA-Z0-9_]{1,32}$/g,
message: '只允许输入1-32位大小写字母、数字、下划线',
pattern: /^[a-zA-Z0-9_.:]{1,32}$/g,
message: '只允许输入1-32位大小写字母、数字、_.:',
};

View File

@@ -1,6 +1,6 @@
<template>
<div class="auth-cert-edit">
<el-dialog title="凭证保存" v-model="dialogVisible" :show-close="false" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
<el-dialog :title="props.title" v-model="dialogVisible" :show-close="false" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
<el-form ref="acForm" :model="state.form" label-width="auto" :rules="rules">
<el-form-item prop="type" label="凭证类型" required>
<el-select @change="changeType" v-model="form.type" placeholder="请选择凭证类型">
@@ -118,6 +118,10 @@ import { ResourceCodePattern } from '@/common/pattern';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({
title: {
type: String,
default: '凭证保存',
},
authCert: {
type: Object,
},
@@ -162,7 +166,7 @@ const rules = {
],
};
const emit = defineEmits(['confirm']);
const emit = defineEmits(['confirm', 'cancel']);
const dialogVisible = defineModel<boolean>('visible', { default: false });
@@ -238,9 +242,10 @@ const getCiphertext = async () => {
const cancelEdit = () => {
dialogVisible.value = false;
emit('cancel');
setTimeout(() => {
state.form = { ...DefaultForm };
acForm.value?.resetFields();
state.form = { ...DefaultForm };
}, 300);
};

View File

@@ -34,9 +34,17 @@
<EnumTag :value="scope.row.type" :enums="AuthCertTypeEnum" />
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
</el-table>
<ResourceAuthCertEdit v-model:visible="state.dvisible" :auth-cert="state.form" @confirm="btnOk" :disable-type="[AuthCertTypeEnum.Public.value]" />
<ResourceAuthCertEdit
v-model:visible="state.dvisible"
:auth-cert="state.form"
@confirm="btnOk"
@cancel="cancelEdit"
:disable-type="[AuthCertTypeEnum.Public.value]"
:disable-ciphertext-type="props.disableCiphertextType"
/>
</div>
</template>
@@ -51,6 +59,9 @@ import { ElMessage } from 'element-plus';
const props = defineProps({
resourceType: { type: Number },
resourceCode: { type: String },
disableCiphertextType: {
type: Array,
},
testConnBtnLoading: { type: Boolean },
});
@@ -109,7 +120,19 @@ const cancelEdit = () => {
const btnOk = async (authCert: any) => {
const isEdit = authCert.id;
if (isEdit || state.idx > 0) {
if (!isEdit) {
const res = await resourceAuthCertApi.listByQuery.request({
name: authCert.name,
pageNum: 1,
pageSize: 100,
});
if (res.total) {
ElMessage.error('该授权凭证名称已存在');
return;
}
}
if (isEdit || state.idx >= 0) {
authCerts.value[state.idx] = authCert;
cancelEdit();
return;
@@ -119,15 +142,6 @@ const btnOk = async (authCert: any) => {
ElMessage.error('该名称或用户名已存在于该账号列表中');
return;
}
const res = await resourceAuthCertApi.listByQuery.request({
name: authCert.name,
pageNum: 1,
pageSize: 100,
});
if (res.total) {
ElMessage.error('该授权凭证名称已存在');
return;
}
authCerts.value.push(authCert);
cancelEdit();

View File

@@ -30,13 +30,14 @@
remote
:remote-method="getInstances"
@change="changeInstance"
v-model="form.instanceId"
v-model="state.selectInstalce"
value-key="id"
placeholder="请输入实例名称搜索并选择实例"
filterable
clearable
class="w100"
>
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id">
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item">
{{ item.name }}
<el-divider direction="vertical" border-style="dashed" />
@@ -47,6 +48,23 @@
</el-select>
</el-form-item>
<el-form-item prop="authCertName" label="授权凭证" required>
<el-select @focus="getAuthCerts" @change="changeAuthCert" v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
<el-option v-for="item in state.authCerts" :key="item.id" :label="`${item.name}`" :value="item.name">
{{ item.name }}
<el-divider direction="vertical" border-style="dashed" />
{{ item.username }}
<el-divider direction="vertical" border-style="dashed" />
<EnumTag :value="item.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
<el-divider direction="vertical" border-style="dashed" />
{{ item.remark }}
</el-option>
</el-select>
</el-form-item>
<el-form-item prop="code" label="编号" required>
<el-input :disabled="form.id" v-model.trim="form.code" placeholder="请输入编号 (数字字母下划线), 不可修改" auto-complete="off"></el-input>
</el-form-item>
@@ -100,6 +118,10 @@ import type { CheckboxValueType } from 'element-plus';
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
import { DbType } from '@/views/ops/db/dialect';
import { ResourceCodePattern } from '@/common/pattern';
import { resourceAuthCertApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
const props = defineProps({
visible: {
@@ -172,6 +194,8 @@ const state = reactive({
dbNamesSelected: [] as any,
dbNamesFiltered: [] as any,
filterString: '',
selectInstalce: {} as any,
authCerts: [] as any,
form: {
id: null,
tagId: [],
@@ -180,6 +204,7 @@ const state = reactive({
database: '',
remark: '',
instanceId: null as any,
authCertName: '',
flowProcdefKey: '',
},
instances: [] as any,
@@ -205,14 +230,27 @@ watch(props, async (newValue: any) => {
}
});
const changeInstance = () => {
const changeInstance = async () => {
state.dbNamesSelected = [];
getAllDatabase();
state.form.instanceId = state.selectInstalce.id;
};
const getAllDatabase = async () => {
const getAuthCerts = async () => {
const res = await resourceAuthCertApi.listByQuery.request({
resourceCode: state.selectInstalce.code,
resourceType: TagResourceTypeEnum.Db.value,
pageSize: 100,
});
state.authCerts = res.list || [];
};
const changeAuthCert = (val: string) => {
getAllDatabase(val);
};
const getAllDatabase = async (authCertName: string) => {
if (state.form.instanceId > 0) {
let dbs = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId });
let dbs = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId, authCertName });
state.allDatabases = dbs;
// 如果是oracle且没查出数据库列表则取实例sid
@@ -238,8 +276,9 @@ const open = async () => {
if (state.form.instanceId) {
// 根据id获取因为需要回显实例名称
await getInstances('', state.form.instanceId);
state.selectInstalce = state.instances[0];
await getAllDatabase(state.form.authCertName);
}
await getAllDatabase();
};
const btnOk = async () => {

View File

@@ -194,7 +194,7 @@
</el-descriptions>
</el-dialog>
<db-edit @val-change="search" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
<db-edit @val-change="search()" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
</div>
</template>

View File

@@ -1,94 +1,111 @@
<template>
<div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="name" label="别名" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-divider content-position="left">基本</el-divider>
<el-form-item prop="code" label="编号" required>
<el-input :disabled="form.id" v-model.trim="form.code" placeholder="请输入编号 (数字字母下划线), 不可修改" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型" required>
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
:key="key"
:value="dbTypeAndDialect[0]"
:label="dbTypeAndDialect[1].getInfo().name"
>
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
{{ dbTypeAndDialect[1].getInfo().name }}
</el-option>
<el-form-item prop="type" label="类型" required>
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
:key="key"
:value="dbTypeAndDialect[0]"
:label="dbTypeAndDialect[1].getInfo().name"
>
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
{{ dbTypeAndDialect[1].getInfo().name }}
</el-option>
<template #prefix>
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
<template #prefix>
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
</template>
</el-select>
</el-form-item>
<el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
<el-col :span="18">
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
</el-col>
</el-form-item>
<el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
<el-input v-model.trim="form.host" placeholder="请输入sqlite文件在服务器的绝对地址"></el-input>
</el-form-item>
<el-form-item v-if="form.type === DbType.oracle" label="SID|服务名">
<el-col :span="5">
<el-select
@change="
() => {
state.extra.serviceName = '';
state.extra.sid = '';
}
"
v-model="state.extra.stype"
placeholder="请选择"
>
<el-option label="服务名" :value="1" />
<el-option label="SID" :value="2" />
</el-select>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="18">
<el-input v-if="state.extra.stype == 1" v-model="state.extra.serviceName" placeholder="请输入服务名"> </el-input>
<el-input v-else v-model="state.extra.sid" placeholder="请输入SID"> </el-input>
</el-col>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
<template v-if="form.type !== DbType.sqlite">
<el-divider content-position="left">账号</el-divider>
<div>
<ResourceAuthCertTableEdit
v-model="form.authCerts"
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Db.value"
:test-conn-btn-loading="testConnBtnLoading"
@test-conn="testConn"
:disable-ciphertext-type="[AuthCertCiphertextTypeEnum.PrivateKey.value]"
/>
</div>
</template>
<!--
<el-form-item v-if="form.type !== DbType.sqlite" prop="username" label="用户名" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item v-if="form.type !== DbType.sqlite" prop="password" label="密码">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
<template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
<template #reference>
<el-link v-auth="'db:instance:save'" @click="getDbPwd" :underline="false" type="primary" class="mr5">原密码 </el-link>
</template>
</el-select>
</el-form-item>
</el-popover>
</template>
</el-input>
</el-form-item> -->
<el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
<el-col :span="18">
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
</el-col>
</el-form-item>
<el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
<el-input v-model.trim="form.host" placeholder="请输入sqlite文件在服务器的绝对地址"></el-input>
</el-form-item>
<el-form-item v-if="form.type === DbType.oracle" label="SID|服务名">
<el-col :span="5">
<el-select
@change="
() => {
state.extra.serviceName = '';
state.extra.sid = '';
}
"
v-model="state.extra.stype"
placeholder="请选择"
>
<el-option label="服务名" :value="1" />
<el-option label="SID" :value="2" />
</el-select>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="18">
<el-input v-if="state.extra.stype == 1" v-model="state.extra.serviceName" placeholder="请输入服务名"> </el-input>
<el-input v-else v-model="state.extra.sid" placeholder="请输入SID"> </el-input>
</el-col>
</el-form-item>
<el-form-item v-if="form.type !== DbType.sqlite" prop="username" label="用户名" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item v-if="form.type !== DbType.sqlite" prop="password" label="密码">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
<template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
<template #reference>
<el-link v-auth="'db:instance:save'" @click="getDbPwd" :underline="false" type="primary" class="mr5"
>原密码
</el-link>
</template>
</el-popover>
</template>
</el-input>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="params" label="连接参数">
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
<!-- <template #suffix>
<el-divider content-position="left">其他</el-divider>
<el-form-item prop="params" label="连接参数">
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
<!-- <template #suffix>
<el-link
target="_blank"
href="https://github.com/go-sql-driver/mysql#parameters"
@@ -98,24 +115,21 @@
>参数参考</el-link
>
</template> -->
</el-input>
</el-form-item>
</el-input>
</el-form-item>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="testConn" :loading="testConnBtnLoading" type="success">测试连接</el-button>
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</el-drawer>
</div>
</template>
@@ -123,11 +137,14 @@
import { reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import { notBlank } from '@/common/assert';
import { RsaEncrypt } from '@/common/rsa';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { ResourceCodePattern } from '@/common/pattern';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
const props = defineProps({
visible: {
@@ -145,6 +162,18 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const rules = {
code: [
{
required: true,
message: '请输入编码',
trigger: ['change', 'blur'],
},
{
pattern: ResourceCodePattern.pattern,
message: ResourceCodePattern.message,
trigger: ['blur'],
},
],
name: [
{
required: true,
@@ -166,13 +195,6 @@ const rules = {
trigger: ['blur'],
},
],
username: [
{
required: true,
message: '请输入用户名',
trigger: ['change', 'blur'],
},
],
sid: [
{
required: true,
@@ -186,29 +208,24 @@ const dbForm: any = ref(null);
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
extra: {} as any, // 连接需要的额外参数json
form: {
id: null,
type: '',
code: '',
name: null,
host: '',
port: null,
username: null,
authCerts: [],
extra: '', // 连接需要的额外参数json字符串
password: null,
params: null,
remark: '',
sshTunnelMachineId: null as any,
},
submitForm: {},
// 原密码
pwd: '',
// 原用户名
oldUserName: null,
submitForm: {} as any,
});
const { dialogVisible, tabActiveName, form, submitForm, pwd } = toRefs(state);
const { dialogVisible, form, submitForm } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
@@ -218,14 +235,12 @@ watch(props, (newValue: any) => {
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.data) {
state.form = { ...newValue.data };
state.oldUserName = state.form.username;
state.extra = JSON.parse(state.form.extra);
} else {
state.form = { port: null, type: DbType.mysql } as any;
state.oldUserName = null;
state.form.authCerts = [];
}
});
@@ -236,13 +251,8 @@ const changeDbType = (val: string) => {
state.extra = {};
};
const getDbPwd = async () => {
state.pwd = await dbApi.getInstancePwd.request({ id: state.form.id });
};
const getReqForm = async () => {
const reqForm = { ...state.form };
reqForm.password = await RsaEncrypt(reqForm.password);
if (!state.form.sshTunnelMachineId) {
reqForm.sshTunnelMachineId = -1;
}
@@ -252,7 +262,7 @@ const getReqForm = async () => {
return reqForm;
};
const testConn = async () => {
const testConn = async (authCert: any) => {
dbForm.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写信息');
@@ -260,20 +270,13 @@ const testConn = async () => {
}
state.submitForm = await getReqForm();
state.submitForm.authCerts = [authCert];
await testConnExec();
ElMessage.success('连接成功');
});
};
const btnOk = async () => {
if (state.form.type !== DbType.sqlite) {
if (!state.form.id) {
notBlank(state.form.password, '新增操作,密码不可为空');
} else if (state.form.username != state.oldUserName) {
notBlank(state.form.password, '已修改用户名,请输入密码');
}
}
dbForm.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写信息');

View File

@@ -3,6 +3,7 @@
<page-table
ref="pageTableRef"
:page-api="dbApi.instances"
:data-handler-fn="handleData"
:searchItems="searchItems"
v-model:query-form="query"
:show-selection="true"
@@ -16,6 +17,10 @@
>
</template>
<template #authCert="{ data }">
<ResourceAuthCert v-model:select-auth-cert="data.selectAuthCert" :auth-certs="data.authCerts" />
</template>
<template #type="{ data }">
<el-tooltip :content="getDbDialect(data.type).getInfo().name" placement="top">
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
@@ -35,7 +40,6 @@
<el-descriptions-item :span="2" label="主机">{{ infoDialog.data.host }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型">{{ infoDialog.data.type }}</el-descriptions-item>
<el-descriptions-item :span="3" label="连接参数">{{ infoDialog.data.params }}</el-descriptions-item>
@@ -71,6 +75,7 @@ import { hasPerms } from '@/components/auth/auth';
import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect } from './dialect';
import { SearchItem } from '@/components/SearchForm';
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
@@ -79,13 +84,14 @@ const perms = {
delInstance: 'db:instance:del',
};
const searchItems = [SearchItem.input('name', '名称')];
const searchItems = [SearchItem.input('code', '编号'), SearchItem.input('name', '名称')];
const columns = ref([
TableColumn.new('code', '编号'),
TableColumn.new('name', '名称'),
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
TableColumn.new('host', 'host:port').setFormatFunc((data: any) => `${data.host}:${data.port}`),
TableColumn.new('username', '用户名'),
TableColumn.new('authCerts[0].username', '授权凭证').isSlot('authCert').setAddWidth(10),
TableColumn.new('params', '连接参数'),
TableColumn.new('remark', '备注'),
]);
@@ -134,6 +140,15 @@ const search = () => {
pageTableRef.value.search();
};
const handleData = (res: any) => {
const dataList = res.list;
// 赋值授权凭证
for (let x of dataList) {
x.selectAuthCert = x.authCerts[0];
}
return res;
};
const showInfo = (info: any) => {
state.infoDialog.data = info;
state.infoDialog.visible = true;

View File

@@ -43,7 +43,6 @@ export const dbApi = {
getInstanceServerInfo: Api.newGet('/instances/{instanceId}/server-info'),
testConn: Api.newPost('/instances/test-conn'),
saveInstance: Api.newPost('/instances'),
getInstancePwd: Api.newGet('/instances/{id}/pwd'),
deleteInstance: Api.newDelete('/instances/{id}'),
// 获取数据库备份列表

View File

@@ -287,7 +287,7 @@ const columns = [
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
TableColumn.new('authCerts[0].username', '授权凭证').isSlot('authCert').setAddWidth(20),
TableColumn.new('authCerts[0].username', '授权凭证').isSlot('authCert').setAddWidth(10),
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
TableColumn.new('remark', '备注'),
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),

View File

@@ -19,9 +19,11 @@
</page-table>
<ResourceAuthCertEdit
:title="editor.title"
v-model:visible="editor.visible"
:auth-cert="editor.authcert"
@confirm="confirmSave"
@cancel="editor.authcert = {}"
:disable-type="state.disableAuthCertType"
:disable-ciphertext-type="state.disableAuthCertCiphertextType"
:resource-edit="false"
@@ -73,7 +75,7 @@ const state = reactive({
paramsFormItem: [] as any,
},
editor: {
title: '授权凭证保存',
title: '添加授权凭证',
visible: false,
authcert: {},
},
@@ -93,6 +95,7 @@ const edit = (data: any) => {
state.disableAuthCertType = [];
state.disableAuthCertCiphertextType = [];
if (data) {
state.editor.title = `编辑授权凭证-[${data.name}]`;
state.editor.authcert = data;
// 如果数据为公共授权凭证,则不允许修改凭证类型
if (data.type == AuthCertTypeEnum.Public.value) {
@@ -103,6 +106,7 @@ const edit = (data: any) => {
state.disableAuthCertType = [AuthCertTypeEnum.Public.value];
}
} else {
state.editor.title = '添加授权凭证';
state.editor.authcert = {
type: AuthCertTypeEnum.Public.value,
ciphertextType: AuthCertCiphertextTypeEnum.Password.value,

View File

@@ -65,9 +65,16 @@
<el-descriptions-item label="类型">
<EnumTag :enums="TagResourceTypeEnum" :value="currentTag.type" />
</el-descriptions-item>
<el-descriptions-item label="code">{{ currentTag.code }}</el-descriptions-item>
<el-descriptions-item label="code路径">{{ currentTag.codePath }}</el-descriptions-item>
<el-descriptions-item label="路径" :span="2">
<span v-for="item in parseTagPath(currentTag.codePath)" :key="item.code">
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon" class="mr2" />
<span> {{ item.code }}</span>
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
</span>
</el-descriptions-item>
<el-descriptions-item label="名称">{{ currentTag.name }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ currentTag.remark }}</el-descriptions-item>
@@ -255,6 +262,38 @@ watch(
}
);
const parseTagPath = (tagPath: string) => {
if (!tagPath) {
return [];
}
const res = [] as any;
const codes = tagPath.split('/');
for (let code of codes) {
const typeAndCode = code.split('|');
if (typeAndCode.length == 1) {
const tagCode = typeAndCode[0];
if (!tagCode) {
continue;
}
res.push({
type: TagResourceTypeEnum.Tag.value,
code: typeAndCode[0],
});
continue;
}
res.push({
type: typeAndCode[0],
code: typeAndCode[1],
});
}
res[res.length - 1].isEnd = true;
return res;
};
const tabChange = () => {
setNowTabData();
};

View File

@@ -28,9 +28,15 @@
</template>
</page-table>
<el-drawer title="团队编辑" v-model="addTeamDialog.visible" :before-close="cancelSaveTeam" :destroy-on-close="true" :close-on-click-modal="false">
<el-drawer
:title="addTeamDialog.form.id ? '编辑团队' : '添加团队'"
v-model="addTeamDialog.visible"
:before-close="cancelSaveTeam"
:destroy-on-close="true"
:close-on-click-modal="false"
>
<template #header>
<DrawerHeader header="团队编辑" :back="cancelSaveTeam" />
<DrawerHeader :header="addTeamDialog.form.id ? '编辑团队' : '添加团队'" :back="cancelSaveTeam" />
</template>
<el-form ref="teamForm" :model="addTeamDialog.form" label-width="auto">
@@ -58,6 +64,7 @@
value: 'id',
label: 'codePath',
children: 'children',
disabled: 'disabled',
}"
@check="tagTreeNodeCheck"
>
@@ -152,7 +159,6 @@ const state = reactive({
currentEditPermissions: false,
tags: [],
addTeamDialog: {
title: '新增团队',
visible: false,
form: { id: 0, name: '', remark: '', tags: [] },
},
@@ -201,16 +207,22 @@ const search = async () => {
};
const showSaveTeamDialog = async (data: any) => {
if (state.tags.length == 0) {
state.tags = await tagApi.getTagTrees.request(null);
}
state.tags = await tagApi.getTagTrees.request(null);
if (data) {
state.addTeamDialog.form.id = data.id;
state.addTeamDialog.form.name = data.name;
state.addTeamDialog.form.remark = data.remark;
state.addTeamDialog.title = `修改 [${data.codePath}] 信息`;
state.addTeamDialog.form.tags = await tagApi.getTeamTagIds.request({ teamId: data.id });
setTimeout(() => {
const checkedNodes = tagTreeRef.value.getCheckedNodes();
console.log('check nodes: ', checkedNodes);
// 禁用选中节点的所有父节点,不可选中
for (let checkNodeData of checkedNodes) {
disableParentNodes(tagTreeRef.value.getNode(checkNodeData.id).parent);
}
}, 200);
}
state.addTeamDialog.visible = true;
@@ -231,8 +243,10 @@ const saveTeam = async () => {
const cancelSaveTeam = () => {
state.addTeamDialog.visible = false;
state.addTeamDialog.form = {} as any;
teamForm.value.resetFields();
setTimeout(() => {
state.addTeamDialog.form = {} as any;
}, 500);
};
const deleteTeam = () => {
@@ -289,34 +303,47 @@ const cancelAddMember = () => {
state.showMemDialog.addVisible = false;
};
const tagTreeNodeCheck = () => {
// const node = tagTreeRef.value.getNode(data.id);
// console.log(node);
// // state.showTagDialog.tagTreeTeams = [16]
// if (node.checked) {
// if (node.parent) {
// console.log(node.parent);
// // removeCheckedTagId(node.parent.key);
// tagTreeRef.value.setChecked(node.parent, false, false);
// }
// // // parentNode = node.parent
// // for (let parentNode of node.parent) {
// // parentNode.setChecked(false);
// // }
// }
// console.log(data);
// console.log(checkInfo);
const tagTreeNodeCheck = (data: any) => {
const node = tagTreeRef.value.getNode(data.id);
console.log('check node: ', node);
if (node.checked) {
// 如果选中了子节点,则需要将父节点全部取消选中,并禁用父节点
unCheckParentNodes(node.parent);
disableParentNodes(node.parent);
} else {
// 如果取消了选中,则需要根据条件恢复父节点的选中状态
disableParentNodes(node.parent, false);
}
};
// function removeCheckedTagId(id: any) {
// console.log(state.showTagDialog.tagTreeTeams);
// for (let i = 0; i < state.showTagDialog.tagTreeTeams.length; i++) {
// if (state.showTagDialog.tagTreeTeams[i] == id) {
// console.log('has id', id);
// state.showTagDialog.tagTreeTeams.splice(i, 1);
// }
// }
// console.log(state.showTagDialog.tagTreeTeams);
// }
const unCheckParentNodes = (node: any) => {
if (!node) {
return;
}
tagTreeRef.value.setChecked(node, false, false);
unCheckParentNodes(node.parent);
};
/**
* 禁用该节点以及所有父节点
* @param node 节点
* @param disable 是否禁用
*/
const disableParentNodes = (node: any, disable = true) => {
if (!node) {
return;
}
if (!disable) {
// 恢复为非禁用状态时,若同层级存在一个选中状态或者禁用状态,则继续禁用 不恢复非禁用状态。
for (let oneLevelNodes of node.childNodes) {
if (oneLevelNodes.checked || oneLevelNodes.data.disabled) {
return;
}
}
}
node.data.disabled = disable;
disableParentNodes(node.parent, disable);
};
</script>
<style lang="scss" scoped></style>

View File

@@ -2,10 +2,10 @@ import { EnumValue } from '@/common/Enum';
// 授权凭证类型
export const AuthCertTypeEnum = {
Public: EnumValue.of(2, '公共凭证').tagTypeSuccess(),
Private: EnumValue.of(1, '普通账号').tagTypeSuccess(),
Privileged: EnumValue.of(11, '特权账号').tagTypeSuccess(),
PrivateDefault: EnumValue.of(12, '默认账号').tagTypeSuccess(),
Public: EnumValue.of(2, '公共凭证').tagTypeSuccess().tagTypeSuccess(),
Private: EnumValue.of(1, '普通账号'),
Privileged: EnumValue.of(11, '特权账号').tagTypeDanger(),
PrivateDefault: EnumValue.of(12, '默认账号').tagTypeWarning(),
};
// 授权凭证密文类型

View File

@@ -16,10 +16,10 @@ const (
// RedisConnExpireTime = 2 * time.Minute
// MongoConnExpireTime = 2 * time.Minute
TagResourceTypeMachine int8 = 1
TagResourceTypeDb int8 = 2
TagResourceTypeRedis int8 = 3
TagResourceTypeMongo int8 = 4
ResourceTypeMachine int8 = 1
ResourceTypeDb int8 = 2
ResourceTypeRedis int8 = 3
ResourceTypeMongo int8 = 4
// 删除机器的事件主题名
DeleteMachineEventTopic = "machine:delete"

View File

@@ -15,7 +15,7 @@ type Dashbord struct {
func (m *Dashbord) Dashbord(rc *req.Ctx) {
accountId := rc.GetLoginAccount().Id
dbNum := len(m.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, ""))
dbNum := len(m.TagTreeApp.GetAccountTagCodes(accountId, consts.ResourceTypeDb, ""))
rc.ResData = collx.M{
"dbNum": dbNum,

View File

@@ -44,7 +44,7 @@ func (d *Db) Dbs(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage[*entity.DbQuery](rc, new(entity.DbQuery))
// 不存在可访问标签id即没有可操作数据
codes := d.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeDb, queryCond.TagPath)
codes := d.TagApp.GetAccountTagCodes(rc.GetLoginAccount().Id, consts.ResourceTypeDb, queryCond.TagPath)
if len(codes) == 0 {
rc.ResData = model.EmptyPageResult[any]()
return
@@ -56,7 +56,7 @@ func (d *Db) Dbs(rc *req.Ctx) {
biz.ErrIsNil(err)
// 填充标签信息
d.TagApp.FillTagInfo(collx.ArrayMap(dbvos, func(dbvo *vo.DbListVO) tagentity.ITagResource {
d.TagApp.FillTagInfo(tagentity.TagType(consts.ResourceTypeDb), collx.ArrayMap(dbvos, func(dbvo *vo.DbListVO) tagentity.ITagResource {
return dbvo
})...)
@@ -280,7 +280,7 @@ func (d *Db) DumpSql(rc *req.Ctx) {
la := rc.GetLoginAccount()
db, err := d.DbApp.GetById(new(entity.Db), dbId)
biz.ErrIsNil(err, "该数据库不存在")
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, d.TagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...), "%s")
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, d.TagApp.ListTagPathByTypeAndCode(consts.ResourceTypeDb, db.Code)...), "%s")
now := time.Now()
filename := fmt.Sprintf("%s-%s.%s.sql%s", db.Name, dbName, now.Format("20060102150405"), extName)

View File

@@ -1,28 +1,41 @@
package api
import (
"mayfly-go/internal/common/consts"
"mayfly-go/internal/db/api/form"
"mayfly-go/internal/db/api/vo"
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/domain/entity"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/cryptox"
"mayfly-go/pkg/utils/collx"
"strconv"
"strings"
)
type Instance struct {
InstanceApp application.Instance `inject:"DbInstanceApp"`
DbApp application.Db `inject:""`
InstanceApp application.Instance `inject:"DbInstanceApp"`
DbApp application.Db `inject:""`
ResourceAuthCertApp tagapp.ResourceAuthCert `inject:""`
}
// Instances 获取数据库实例信息
// @router /api/instances [get]
func (d *Instance) Instances(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage[*entity.InstanceQuery](rc, new(entity.InstanceQuery))
res, err := d.InstanceApp.GetPageList(queryCond, page, new([]vo.InstanceListVO))
var instvos []*vo.InstanceListVO
res, err := d.InstanceApp.GetPageList(queryCond, page, &instvos)
biz.ErrIsNil(err)
// 填充授权凭证信息
d.ResourceAuthCertApp.FillAuthCert(consts.ResourceTypeDb, collx.ArrayMap(instvos, func(vos *vo.InstanceListVO) tagentity.IAuthCert {
return vos
})...)
rc.ResData = res
}
@@ -30,12 +43,7 @@ func (d *Instance) TestConn(rc *req.Ctx) {
form := &form.InstanceForm{}
instance := req.BindJsonAndCopyTo[*entity.DbInstance](rc, form, new(entity.DbInstance))
// 密码解密,并使用解密后的赋值
originPwd, err := cryptox.DefaultRsaDecrypt(form.Password, true)
biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
instance.Password = originPwd
biz.ErrIsNil(d.InstanceApp.TestConn(instance))
biz.ErrIsNil(d.InstanceApp.TestConn(instance, form.AuthCerts[0]))
}
// SaveInstance 保存数据库实例信息
@@ -44,15 +52,11 @@ func (d *Instance) SaveInstance(rc *req.Ctx) {
form := &form.InstanceForm{}
instance := req.BindJsonAndCopyTo[*entity.DbInstance](rc, form, new(entity.DbInstance))
// 密码解密,并使用解密后的赋值
originPwd, err := cryptox.DefaultRsaDecrypt(form.Password, true)
biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
instance.Password = originPwd
// 密码脱敏记录日志
form.Password = "****"
rc.ReqParam = form
biz.ErrIsNil(d.InstanceApp.Save(rc.MetaCtx, instance))
biz.ErrIsNil(d.InstanceApp.SaveDbInstance(rc.MetaCtx, &application.SaveDbInstanceParam{
DbInstance: instance,
AuthCerts: form.AuthCerts,
}))
}
// GetInstance 获取数据库实例密码,由于数据库是加密存储,故提供该接口展示原文密码
@@ -61,20 +65,9 @@ func (d *Instance) GetInstance(rc *req.Ctx) {
dbId := getInstanceId(rc)
dbEntity, err := d.InstanceApp.GetById(new(entity.DbInstance), dbId)
biz.ErrIsNil(err, "获取数据库实例错误")
dbEntity.Password = ""
rc.ResData = dbEntity
}
// GetInstancePwd 获取数据库实例密码,由于数据库是加密存储,故提供该接口展示原文密码
// @router /api/instances/:instance/pwd [GET]
func (d *Instance) GetInstancePwd(rc *req.Ctx) {
instanceId := getInstanceId(rc)
instanceEntity, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "Password")
biz.ErrIsNil(err, "获取数据库实例错误")
biz.ErrIsNil(instanceEntity.PwdDecrypt())
rc.ResData = instanceEntity.Password
}
// DeleteInstance 删除数据库实例信息
// @router /api/instances/:instance [DELETE]
func (d *Instance) DeleteInstance(rc *req.Ctx) {
@@ -94,10 +87,13 @@ func (d *Instance) DeleteInstance(rc *req.Ctx) {
// 获取数据库实例的所有数据库名
func (d *Instance) GetDatabaseNames(rc *req.Ctx) {
instanceId := getInstanceId(rc)
instance, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "Password")
authCertName := rc.Query("authCertName")
biz.NotEmpty(authCertName, "授权凭证名不能为空")
instance, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId)
biz.ErrIsNil(err, "获取数据库实例错误")
biz.ErrIsNil(instance.PwdDecrypt())
res, err := d.InstanceApp.GetDatabases(instance)
res, err := d.InstanceApp.GetDatabases(instance, authCertName)
biz.ErrIsNil(err)
rc.ResData = res
}

View File

@@ -1,15 +1,18 @@
package form
import tagentity "mayfly-go/internal/tag/domain/entity"
type InstanceForm struct {
Id uint64 `json:"id"`
Code string `binding:"pattern=resource_code" json:"code"`
Name string `binding:"required" json:"name"`
Type string `binding:"required" json:"type"` // 类型mysql oracle等
Host string `binding:"required" json:"host"`
Port int `json:"port"`
Extra string `json:"extra"`
Username string `json:"username"`
Password string `json:"password"`
Params string `json:"params"`
Remark string `json:"remark"`
SshTunnelMachineId int `json:"sshTunnelMachineId"`
AuthCerts []*tagentity.ResourceAuthCert `json:"authCerts" binding:"required"` // 资产授权凭证信息列表
}

View File

@@ -15,11 +15,12 @@ type DbListVO struct {
Remark *string `json:"remark"`
InstanceId *int64 `json:"instanceId"`
AuthCertName string `json:"authCertName"`
Username string `json:"username"`
InstanceName *string `json:"instanceName"`
InstanceType *string `json:"type"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
FlowProcdefKey string `json:"flowProcdefKey"`

View File

@@ -1,16 +1,21 @@
package vo
import "time"
import (
tagentity "mayfly-go/internal/tag/domain/entity"
"time"
)
type InstanceListVO struct {
tagentity.AuthCerts // 授权凭证信息
Id *int64 `json:"id"`
Code string `json:"code"`
Name *string `json:"name"`
Host *string `json:"host"`
Port *int `json:"port"`
Type *string `json:"type"`
Params string `json:"params"`
Extra string `json:"extra"`
Username *string `json:"username"`
Remark *string `json:"remark"`
CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"`
@@ -22,3 +27,7 @@ type InstanceListVO struct {
SshTunnelMachineId int `json:"sshTunnelMachineId"`
}
func (i *InstanceListVO) GetCode() string {
return i.Code
}

View File

@@ -15,7 +15,6 @@ import (
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/structx"
"sort"
"strings"
"time"
@@ -86,10 +85,10 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db, tagIds ...u
return d.Tx(ctx, func(ctx context.Context) error {
return d.Insert(ctx, dbEntity)
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceCode: dbEntity.Code,
ResourceType: tagentity.TagTypeDb,
TagIds: tagIds,
return d.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Code: dbEntity.Code,
Type: tagentity.TagTypeDb,
ParentTagIds: tagIds,
})
})
}
@@ -126,10 +125,10 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db, tagIds ...u
return d.Tx(ctx, func(ctx context.Context) error {
return d.UpdateById(ctx, dbEntity)
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceCode: old.Code,
ResourceType: tagentity.TagTypeDb,
TagIds: tagIds,
return d.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Code: old.Code,
Type: tagentity.TagTypeDb,
ParentTagIds: tagIds,
})
})
}
@@ -153,9 +152,9 @@ func (d *dbAppImpl) Delete(ctx context.Context, id uint64) error {
// 删除该库下用户保存的所有sql信息
return d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: id})
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceCode: db.Code,
ResourceType: tagentity.TagTypeDb,
return d.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Code: db.Code,
Type: tagentity.TagTypeDb,
})
})
}
@@ -172,11 +171,13 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
return nil, errorx.NewBiz("数据库实例不存在")
}
// 密码解密
if err := instance.PwdDecrypt(); err != nil {
return nil, errorx.NewBiz(err.Error())
di, err := d.dbInstanceApp.ToDbInfo(instance, db.AuthCertName, dbName)
if err != nil {
return nil, err
}
di := toDbInfo(instance, dbId, dbName, d.tagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...)
di.TagPath = d.tagApp.ListTagPathByTypeAndCode(consts.ResourceTypeDb, db.Code)
di.Id = db.Id
if db.FlowProcdefKey != nil {
di.FlowProcdefKey = *db.FlowProcdefKey
}
@@ -323,14 +324,3 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *DumpDbReq) error {
return nil
}
func toDbInfo(instance *entity.DbInstance, dbId uint64, database string, tagPath ...string) *dbi.DbInfo {
di := new(dbi.DbInfo)
di.InstanceId = instance.Id
di.Id = dbId
di.Database = database
di.TagPath = tagPath
structx.Copy(di, instance)
return di
}

View File

@@ -3,18 +3,27 @@ package application
import (
"context"
"errors"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/db/dbm"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/structx"
"gorm.io/gorm"
)
type SaveDbInstanceParam struct {
DbInstance *entity.DbInstance
AuthCerts []*tagentity.ResourceAuthCert
}
type Instance interface {
base.App[*entity.DbInstance]
@@ -23,23 +32,27 @@ type Instance interface {
Count(condition *entity.InstanceQuery) int64
TestConn(instanceEntity *entity.DbInstance) error
TestConn(instanceEntity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) error
Save(ctx context.Context, instanceEntity *entity.DbInstance) error
SaveDbInstance(ctx context.Context, instance *SaveDbInstanceParam) error
// Delete 删除数据库信息
Delete(ctx context.Context, id uint64) error
// GetDatabases 获取数据库实例的所有数据库列表
GetDatabases(entity *entity.DbInstance) ([]string, error)
GetDatabases(entity *entity.DbInstance, authCertName string) ([]string, error)
// ToDbInfo 根据实例与授权凭证返回对应的DbInfo
ToDbInfo(instance *entity.DbInstance, authCertName string, database string) (*dbi.DbInfo, error)
}
type instanceAppImpl struct {
base.AppImpl[*entity.DbInstance, repository.Instance]
dbApp Db `inject:"DbApp"`
backupApp *DbBackupApp `inject:"DbBackupApp"`
restoreApp *DbRestoreApp `inject:"DbRestoreApp"`
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"ResourceAuthCertApp"`
dbApp Db `inject:"DbApp"`
backupApp *DbBackupApp `inject:"DbBackupApp"`
restoreApp *DbRestoreApp `inject:"DbRestoreApp"`
}
// 注入DbInstanceRepo
@@ -56,9 +69,23 @@ func (app *instanceAppImpl) Count(condition *entity.InstanceQuery) int64 {
return app.CountByCond(condition)
}
func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance) error {
func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) error {
instanceEntity.Network = instanceEntity.GetNetwork()
dbConn, err := dbm.Conn(toDbInfo(instanceEntity, 0, "", ""))
if authCert.Id != 0 {
// 密文可能被清除,故需要重新获取
authCert, _ = app.resourceAuthCertApp.GetAuthCert(authCert.Name)
} else {
if authCert.CiphertextType == tagentity.AuthCertCiphertextTypePublic {
publicAuthCert, err := app.resourceAuthCertApp.GetAuthCert(authCert.Ciphertext)
if err != nil {
return err
}
authCert = publicAuthCert
}
}
dbConn, err := dbm.Conn(app.toDbInfoByAc(instanceEntity, authCert, ""))
if err != nil {
return err
}
@@ -66,47 +93,65 @@ func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance) error {
return nil
}
func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbInstance) error {
func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *SaveDbInstanceParam) error {
instanceEntity := instance.DbInstance
// 默认tcp连接
instanceEntity.Network = instanceEntity.GetNetwork()
resourceType := consts.ResourceTypeDb
authCerts := instance.AuthCerts
if len(authCerts) == 0 {
return errorx.NewBiz("授权凭证信息不能为空")
}
// 查找是否存在该库
oldInstance := &entity.DbInstance{
Host: instanceEntity.Host,
Port: instanceEntity.Port,
Username: instanceEntity.Username,
SshTunnelMachineId: instanceEntity.SshTunnelMachineId,
}
err := app.GetBy(oldInstance)
if instanceEntity.Id == 0 {
if instanceEntity.Type != string(dbi.DbTypeSqlite) && instanceEntity.Password == "" {
return errorx.NewBiz("密码不能为空")
}
if err == nil {
return errorx.NewBiz("该数据库实例已存在")
}
if err := instanceEntity.PwdEncrypt(); err != nil {
return errorx.NewBiz(err.Error())
if app.CountByCond(&entity.DbInstance{Code: instanceEntity.Code}) > 0 {
return errorx.NewBiz("该编码已存在")
}
return app.Insert(ctx, instanceEntity)
return app.Tx(ctx, func(ctx context.Context) error {
return app.Insert(ctx, instanceEntity)
}, func(ctx context.Context) error {
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: instanceEntity.Code,
ResourceType: tagentity.TagType(resourceType),
AuthCerts: authCerts,
})
})
}
// 如果存在该库,则校验修改的库是否为该库
if err == nil && oldInstance.Id != instanceEntity.Id {
return errorx.NewBiz("该数据库实例已存在")
}
if err := instanceEntity.PwdEncrypt(); err != nil {
return errorx.NewBiz(err.Error())
}
return app.UpdateById(ctx, instanceEntity)
return app.Tx(ctx, func(ctx context.Context) error {
return app.UpdateById(ctx, instanceEntity)
}, func(ctx context.Context) error {
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: oldInstance.Code,
ResourceType: tagentity.TagType(resourceType),
AuthCerts: authCerts,
})
})
}
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
instance, err := app.GetById(new(entity.DbInstance), instanceId, "name")
biz.ErrIsNil(err, "获取数据库实例错误数据库实例ID为: %d", instance.Id)
if err != nil {
return errorx.NewBiz("获取数据库实例错误数据库实例ID为: %d", instance.Id)
}
restore := &entity.DbRestore{
DbInstanceId: instanceId,
@@ -147,13 +192,25 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
}
return app.DeleteById(ctx, instanceId)
return app.Tx(ctx, func(ctx context.Context) error {
return app.DeleteById(ctx, instanceId)
}, func(ctx context.Context) error {
// 删除该实例关联的授权凭证信息
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: instance.Code,
ResourceType: tagentity.TagType(consts.ResourceTypeDb),
})
})
}
func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance) ([]string, error) {
func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance, authCertName string) ([]string, error) {
ed.Network = ed.GetNetwork()
dbi, err := app.ToDbInfo(ed, authCertName, "")
if err != nil {
return nil, err
}
dbConn, err := dbm.Conn(toDbInfo(ed, 0, "", ""))
dbConn, err := dbm.Conn(dbi)
if err != nil {
return nil, err
}
@@ -161,3 +218,23 @@ func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance) ([]string, error
return dbConn.GetMetaData().GetDbNames()
}
func (app *instanceAppImpl) ToDbInfo(instance *entity.DbInstance, authCertName string, database string) (*dbi.DbInfo, error) {
ac, err := app.resourceAuthCertApp.GetAuthCert(authCertName)
if err != nil {
return nil, err
}
return app.toDbInfoByAc(instance, ac, database), nil
}
func (app *instanceAppImpl) toDbInfoByAc(instance *entity.DbInstance, ac *tagentity.ResourceAuthCert, database string) *dbi.DbInfo {
di := new(dbi.DbInfo)
di.InstanceId = instance.Id
di.Database = database
structx.Copy(di, instance)
di.Username = ac.Username
di.Password = ac.Ciphertext
return di
}

View File

@@ -12,5 +12,6 @@ type Db struct {
Database string `orm:"column(database)" json:"database"`
Remark string `json:"remark"`
InstanceId uint64
AuthCertName string `json:"authCertName"`
FlowProcdefKey *string `json:"flowProcdefKey"` // 审批流-流程定义key有值则说明关键操作需要进行审批执行,使用指针为了方便更新空字符串(取消流程审批)
}

View File

@@ -1,24 +1,21 @@
package entity
import (
"errors"
"fmt"
"mayfly-go/internal/common/utils"
"mayfly-go/pkg/model"
)
type DbInstance struct {
model.Model
Code string `json:"code"`
Name string `json:"name"`
Type string `json:"type"` // 类型mysql oracle等
Host string `json:"host"`
Port int `json:"port"`
Network string `json:"network"`
Extra *string `json:"extra"` // 连接需要的其他额外参数json格式, 如oracle需要sid等
Username string `json:"username"`
Password string `json:"-"`
Params *string `json:"params"`
Extra *string `json:"extra"` // 连接需要的其他额外参数json格式, 如oracle需要sid等
Params *string `json:"params"` // 使用指针类型,可更新为零值(空字符串)
Remark string `json:"remark"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
}
@@ -39,23 +36,3 @@ func (d *DbInstance) GetNetwork() string {
}
return fmt.Sprintf("%s+ssh:%d", d.Type, d.SshTunnelMachineId)
}
func (d *DbInstance) PwdEncrypt() error {
// 密码替换为加密后的密码
password, err := utils.PwdAesEncrypt(d.Password)
if err != nil {
return errors.New("加密数据库密码失败")
}
d.Password = password
return nil
}
func (d *DbInstance) PwdDecrypt() error {
// 密码替换为解密后的密码
password, err := utils.PwdAesDecrypt(d.Password)
if err != nil {
return errors.New("解密数据库密码失败")
}
d.Password = password
return nil
}

View File

@@ -4,6 +4,7 @@ package entity
type InstanceQuery struct {
Id uint64 `json:"id" form:"id"`
Name string `json:"name" form:"name"`
Code string `json:"code" form:"code"`
Host string `json:"host" form:"host"`
}

View File

@@ -19,8 +19,8 @@ func newDbRepo() repository.Db {
// 分页获取数据库信息列表
func (d *dbRepoImpl) GetDbList(condition *entity.DbQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQueryWithTableName("t_db db").
Select("db.*, inst.name instance_name, inst.type instance_type, inst.host, inst.port, inst.username ").
Joins("JOIN t_db_instance inst ON db.instance_id = inst.id").
Select("db.*, inst.name instance_name, inst.type instance_type, inst.host, inst.port, rac.username ").
Joins("JOIN t_db_instance inst ON db.instance_id = inst.id JOIN t_resource_auth_cert rac ON inst.code = rac.resource_code AND rac.resource_type = 2").
Eq("db.instance_id", condition.InstanceId).
Eq("db.id", condition.Id).
Like("db.database", condition.Database).

View File

@@ -21,6 +21,7 @@ func (d *instanceRepoImpl) GetInstanceList(condition *entity.InstanceQuery, page
qd := gormx.NewQuery(new(entity.DbInstance)).
Eq("id", condition.Id).
Eq("host", condition.Host).
Like("name", condition.Name)
Like("name", condition.Name).
Like("code", condition.Code)
return gormx.PageQuery(qd, pageParam, toEntity)
}

View File

@@ -25,8 +25,6 @@ func InitInstanceRouter(router *gin.RouterGroup) {
req.NewGet(":instanceId", d.GetInstance),
req.NewGet(":instanceId/pwd", d.GetInstancePwd),
// 获取数据库实例的所有数据库名
req.NewGet(":instanceId/databases", d.GetDatabaseNames),

View File

@@ -5,13 +5,13 @@ import tagentity "mayfly-go/internal/tag/domain/entity"
type MachineForm struct {
Id uint64 `json:"id"`
Protocol int `json:"protocol" binding:"required"`
Code string `json:"code" binding:"required"`
Code string `json:"code" binding:"pattern=resource_code"`
Name string `json:"name" binding:"required"`
Ip string `json:"ip" binding:"required"` // IP地址
Port int `json:"port" binding:"required"` // 端口号
TagId []uint64 `json:"tagId" binding:"required"`
AuthCerts []*tagentity.ResourceAuthCert // 资产授权凭证信息列表
AuthCerts []*tagentity.ResourceAuthCert `json:"authCerts" binding:"required"` // 资产授权凭证信息列表
Remark string `json:"remark"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id

View File

@@ -3,6 +3,7 @@ package api
import (
"encoding/base64"
"fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/machine/api/form"
"mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/application"
@@ -62,12 +63,12 @@ func (m *Machine) Machines(rc *req.Ctx) {
}
// 填充标签信息
m.TagApp.FillTagInfo(collx.ArrayMap(machinevos, func(mvo *vo.MachineVO) tagentity.ITagResource {
m.TagApp.FillTagInfo(tagentity.TagType(consts.ResourceTypeMachine), collx.ArrayMap(machinevos, func(mvo *vo.MachineVO) tagentity.ITagResource {
return mvo
})...)
// 填充授权凭证信息
m.ResourceAuthCertApp.FillAuthCert(authCerts, collx.ArrayMap(machinevos, func(mvo *vo.MachineVO) tagentity.IAuthCert {
m.ResourceAuthCertApp.FillAuthCertByAcs(authCerts, collx.ArrayMap(machinevos, func(mvo *vo.MachineVO) tagentity.IAuthCert {
return mvo
})...)

View File

@@ -17,7 +17,6 @@ import (
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/scheduler"
"time"
)
type SaveMachineParam struct {
@@ -66,7 +65,6 @@ type Machine interface {
type machineAppImpl struct {
base.AppImpl[*entity.Machine, repository.Machine]
// authCertApp AuthCert `inject:"AuthCertApp"`
tagApp tagapp.TagTree `inject:"TagTreeApp"`
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"ResourceAuthCertApp"`
}
@@ -87,6 +85,10 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, param *SaveMachinePara
authCerts := param.AuthCerts
resourceType := tagentity.TagTypeMachine
if len(authCerts) == 0 {
return errorx.NewBiz("授权凭证信息不能为空")
}
oldMachine := &entity.Machine{
Ip: me.Ip,
Port: me.Port,
@@ -94,7 +96,6 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, param *SaveMachinePara
}
err := m.GetBy(oldMachine)
if me.Id == 0 {
if err == nil {
return errorx.NewBiz("该机器信息已存在")
@@ -109,16 +110,23 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, param *SaveMachinePara
if err := m.Tx(ctx, func(ctx context.Context) error {
return m.Insert(ctx, me)
}, func(ctx context.Context) error {
return m.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
return m.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Code: me.Code,
Type: resourceType,
ParentTagIds: tagIds,
})
}, func(ctx context.Context) error {
return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: me.Code,
ResourceType: resourceType,
TagIds: tagIds,
AuthCerts: authCerts,
})
}); err != nil {
return err
}
return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
// 事务问题,导致可能删除或新增的标签数据不可见,造成数据错误,故单独执行。若万一失败,可授权凭证管理处添加
return m.resourceAuthCertApp.RelateAuthCert2ResourceTag(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: me.Code,
ResourceType: resourceType,
AuthCerts: authCerts,
@@ -141,31 +149,44 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, param *SaveMachinePara
if err := m.Tx(ctx, func(ctx context.Context) error {
return m.UpdateById(ctx, me)
}, func(ctx context.Context) error {
return m.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceCode: oldMachine.Code,
ResourceType: resourceType,
TagIds: tagIds,
return m.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Code: oldMachine.Code,
Type: resourceType,
ParentTagIds: tagIds,
})
}); err != nil {
return err
}
return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: oldMachine.Code,
ResourceType: resourceType,
AuthCerts: authCerts,
return m.Tx(ctx, func(ctx context.Context) error {
return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: oldMachine.Code,
ResourceType: resourceType,
AuthCerts: authCerts,
})
}, func(ctx context.Context) error {
return m.resourceAuthCertApp.RelateAuthCert2ResourceTag(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: oldMachine.Code,
ResourceType: resourceType,
AuthCerts: authCerts,
})
})
}
func (m *machineAppImpl) TestConn(me *entity.Machine, authCert *tagentity.ResourceAuthCert) error {
me.Id = 0
if authCert.CiphertextType == tagentity.AuthCertCiphertextTypePublic {
publicAuthCert, err := m.resourceAuthCertApp.GetAuthCert(authCert.Ciphertext)
if err != nil {
return err
if authCert.Id != 0 {
// 密文可能被清除,故需要重新获取
authCert, _ = m.resourceAuthCertApp.GetAuthCert(authCert.Name)
} else {
if authCert.CiphertextType == tagentity.AuthCertCiphertextTypePublic {
publicAuthCert, err := m.resourceAuthCertApp.GetAuthCert(authCert.Ciphertext)
if err != nil {
return err
}
authCert = publicAuthCert
}
authCert = publicAuthCert
}
mi, err := m.toMi(me, authCert)
@@ -208,12 +229,17 @@ func (m *machineAppImpl) Delete(ctx context.Context, id uint64) error {
func(ctx context.Context) error {
return m.DeleteById(ctx, id)
}, func(ctx context.Context) error {
return m.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
return m.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Code: machine.Code,
Type: resourceType,
})
}, func(ctx context.Context) error {
return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: machine.Code,
ResourceType: resourceType,
})
}, func(ctx context.Context) error {
return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
return m.resourceAuthCertApp.RelateAuthCert2ResourceTag(ctx, &tagapp.RelateAuthCertParam{
ResourceCode: machine.Code,
ResourceType: resourceType,
})
@@ -267,13 +293,9 @@ func (m *machineAppImpl) TimerUpdateStats() {
}()
logx.Debugf("定时获取机器[id=%d]状态信息开始", mid)
cli, err := m.GetCli(mid)
// ssh获取客户端失败则更新机器状态为禁用
if err != nil {
updateMachine := &entity.Machine{Status: entity.MachineStatusDisable}
updateMachine.Id = mid
now := time.Now()
updateMachine.UpdateTime = &now
m.UpdateById(context.TODO(), updateMachine)
logx.Errorf("定时获取机器[id=%d]状态信息失败, 获取机器cli失败: %s", mid, err.Error())
return
}
cache.SaveMachineStats(mid, cli.GetAllStats())
logx.Debugf("定时获取机器[id=%d]状态信息结束", mid)
@@ -336,7 +358,7 @@ func (m *machineAppImpl) toMi(me *entity.Machine, authCert *tagentity.ResourceAu
mi.Name = me.Name
mi.Ip = me.Ip
mi.Port = me.Port
mi.TagPath = m.tagApp.ListTagPathByResource(int8(tagentity.TagTypeMachineAuthCert), authCert.Name)
mi.TagPath = m.tagApp.ListTagPathByTypeAndCode(int8(tagentity.TagTypeMachineAuthCert), authCert.Name)
mi.EnableRecorder = me.EnableRecorder
mi.Protocol = me.Protocol

View File

@@ -28,7 +28,7 @@ func (m *machineRepoImpl) GetMachineList(condition *entity.MachineQuery, pagePar
Like("ip", condition.Ip).
Like("name", condition.Name).
In("code", condition.Codes).
Eq("code", condition.Code)
Like("code", condition.Code)
// 只查询ssh服务器
if condition.Ssh == entity.MachineProtocolSsh {

View File

@@ -13,7 +13,7 @@ type Dashbord struct {
func (m *Dashbord) Dashbord(rc *req.Ctx) {
accountId := rc.GetLoginAccount().Id
mongoNum := len(m.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMongo, ""))
mongoNum := len(m.TagTreeApp.GetAccountTagCodes(accountId, consts.ResourceTypeMongo, ""))
rc.ResData = collx.M{
"mongoNum": mongoNum,

View File

@@ -31,7 +31,7 @@ func (m *Mongo) Mongos(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage[*entity.MongoQuery](rc, new(entity.MongoQuery))
// 不存在可访问标签id即没有可操作数据
codes := m.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeMongo, queryCond.TagPath)
codes := m.TagApp.GetAccountTagCodes(rc.GetLoginAccount().Id, consts.ResourceTypeMongo, queryCond.TagPath)
if len(codes) == 0 {
rc.ResData = model.EmptyPageResult[any]()
return
@@ -43,7 +43,7 @@ func (m *Mongo) Mongos(rc *req.Ctx) {
biz.ErrIsNil(err)
// 填充标签信息
m.TagApp.FillTagInfo(collx.ArrayMap(mongovos, func(mvo *vo.Mongo) tagentity.ITagResource {
m.TagApp.FillTagInfo(tagentity.TagType(consts.ResourceTypeMongo), collx.ArrayMap(mongovos, func(mvo *vo.Mongo) tagentity.ITagResource {
return mvo
})...)

View File

@@ -59,9 +59,9 @@ func (d *mongoAppImpl) Delete(ctx context.Context, id uint64) error {
return d.DeleteById(ctx, id)
},
func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: tagentity.TagTypeMongo,
ResourceCode: mongoEntity.Code,
return d.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Type: tagentity.TagTypeMongo,
Code: mongoEntity.Code,
})
})
}
@@ -90,10 +90,10 @@ func (d *mongoAppImpl) SaveMongo(ctx context.Context, m *entity.Mongo, tagIds ..
return d.Tx(ctx, func(ctx context.Context) error {
return d.Insert(ctx, m)
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: tagentity.TagTypeMongo,
ResourceCode: m.Code,
TagIds: tagIds,
return d.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Type: tagentity.TagTypeMongo,
Code: m.Code,
ParentTagIds: tagIds,
})
})
}
@@ -113,10 +113,10 @@ func (d *mongoAppImpl) SaveMongo(ctx context.Context, m *entity.Mongo, tagIds ..
return d.Tx(ctx, func(ctx context.Context) error {
return d.UpdateById(ctx, m)
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: tagentity.TagTypeMongo,
ResourceCode: oldMongo.Code,
TagIds: tagIds,
return d.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Type: tagentity.TagTypeMongo,
Code: oldMongo.Code,
ParentTagIds: tagIds,
})
})
}
@@ -127,6 +127,6 @@ func (d *mongoAppImpl) GetMongoConn(id uint64) (*mgm.MongoConn, error) {
if err != nil {
return nil, errorx.NewBiz("mongo信息不存在")
}
return me.ToMongoInfo(d.tagApp.ListTagPathByResource(consts.TagResourceTypeMongo, me.Code)...), nil
return me.ToMongoInfo(d.tagApp.ListTagPathByTypeAndCode(consts.ResourceTypeMongo, me.Code)...), nil
})
}

View File

@@ -13,7 +13,7 @@ type Dashbord struct {
func (m *Dashbord) Dashbord(rc *req.Ctx) {
accountId := rc.GetLoginAccount().Id
redisNum := len(m.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeRedis, ""))
redisNum := len(m.TagTreeApp.GetAccountTagCodes(accountId, consts.ResourceTypeRedis, ""))
rc.ResData = collx.M{
"redisNum": redisNum,

View File

@@ -31,7 +31,7 @@ func (r *Redis) RedisList(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage[*entity.RedisQuery](rc, new(entity.RedisQuery))
// 不存在可访问标签id即没有可操作数据
codes := r.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeRedis, queryCond.TagPath)
codes := r.TagApp.GetAccountTagCodes(rc.GetLoginAccount().Id, consts.ResourceTypeRedis, queryCond.TagPath)
if len(codes) == 0 {
rc.ResData = model.EmptyPageResult[any]()
return
@@ -43,7 +43,7 @@ func (r *Redis) RedisList(rc *req.Ctx) {
biz.ErrIsNil(err)
// 填充标签信息
r.TagApp.FillTagInfo(collx.ArrayMap(redisvos, func(rvo *vo.Redis) tagentity.ITagResource {
r.TagApp.FillTagInfo(tagentity.TagType(consts.ResourceTypeRedis), collx.ArrayMap(redisvos, func(rvo *vo.Redis) tagentity.ITagResource {
return rvo
})...)

View File

@@ -107,10 +107,10 @@ func (r *redisAppImpl) SaveRedis(ctx context.Context, re *entity.Redis, tagIds .
return r.Tx(ctx, func(ctx context.Context) error {
return r.Insert(ctx, re)
}, func(ctx context.Context) error {
return r.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: tagenttiy.TagTypeRedis,
ResourceCode: re.Code,
TagIds: tagIds,
return r.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Type: tagenttiy.TagTypeRedis,
Code: re.Code,
ParentTagIds: tagIds,
})
})
}
@@ -138,10 +138,10 @@ func (r *redisAppImpl) SaveRedis(ctx context.Context, re *entity.Redis, tagIds .
return r.Tx(ctx, func(ctx context.Context) error {
return r.UpdateById(ctx, re)
}, func(ctx context.Context) error {
return r.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: tagenttiy.TagTypeRedis,
ResourceCode: oldRedis.Code,
TagIds: tagIds,
return r.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Type: tagenttiy.TagTypeRedis,
Code: oldRedis.Code,
ParentTagIds: tagIds,
})
})
}
@@ -161,9 +161,9 @@ func (r *redisAppImpl) Delete(ctx context.Context, id uint64) error {
return r.Tx(ctx, func(ctx context.Context) error {
return r.DeleteById(ctx, id)
}, func(ctx context.Context) error {
return r.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: tagenttiy.TagTypeRedis,
ResourceCode: re.Code,
return r.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
Type: tagenttiy.TagTypeRedis,
Code: re.Code,
})
})
}
@@ -179,7 +179,7 @@ func (r *redisAppImpl) GetRedisConn(id uint64, db int) (*rdm.RedisConn, error) {
if err := re.PwdDecrypt(); err != nil {
return nil, errorx.NewBiz(err.Error())
}
return re.ToRedisInfo(db, r.tagApp.ListTagPathByResource(consts.TagResourceTypeRedis, re.Code)...), nil
return re.ToRedisInfo(db, r.tagApp.ListTagPathByTypeAndCode(consts.ResourceTypeRedis, re.Code)...), nil
})
}

View File

@@ -87,10 +87,10 @@ func (p *TagTree) DelTagTree(rc *req.Ctx) {
func (p *TagTree) TagResources(rc *req.Ctx) {
resourceType := int8(rc.PathParamInt("rtype"))
accountId := rc.GetLoginAccount().Id
tagResources := p.TagTreeApp.GetAccountTagResources(accountId, &entity.TagTreeQuery{Type: entity.TagType(resourceType)})
tagResources := p.TagTreeApp.GetAccountTags(accountId, &entity.TagTreeQuery{Type: entity.TagType(resourceType)})
tagPath2Resource := collx.ArrayToMap[*entity.TagTree, string](tagResources, func(tagResource *entity.TagTree) string {
return tagResource.GetParentPath(1)
return tagResource.GetTagPath()
})
tagPaths := collx.MapKeys(tagPath2Resource)
@@ -110,8 +110,8 @@ func (p *TagTree) CountTagResource(rc *req.Ctx) {
rc.ResData = collx.M{
"machine": len(collx.ArrayDeduplicate(machineCodes)),
"db": len(p.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, tagPath)),
"redis": len(p.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeRedis, tagPath)),
"mongo": len(p.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMongo, tagPath)),
"db": len(p.TagTreeApp.GetAccountTagCodes(accountId, consts.ResourceTypeDb, tagPath)),
"redis": len(p.TagTreeApp.GetAccountTagCodes(accountId, consts.ResourceTypeRedis, tagPath)),
"mongo": len(p.TagTreeApp.GetAccountTagCodes(accountId, consts.ResourceTypeMongo, tagPath)),
}
}

View File

@@ -23,9 +23,12 @@ type RelateAuthCertParam struct {
type ResourceAuthCert interface {
base.App[*entity.ResourceAuthCert]
// RelateAuthCert 保存资源授权凭证信息,不可放于事务中
// RelateAuthCert 关联资源授权凭证信息
RelateAuthCert(ctx context.Context, param *RelateAuthCertParam) error
// RelateAuthCert2ResourceTag 关联授权凭证标签至指定资源标签下
RelateAuthCert2ResourceTag(ctx context.Context, param *RelateAuthCertParam) error
// SaveAuthCert 保存授权凭证信息
SaveAuthCert(ctx context.Context, rac *entity.ResourceAuthCert) error
@@ -40,9 +43,15 @@ type ResourceAuthCert interface {
// GetAccountAuthCert 获取账号有权限操作的授权凭证信息
GetAccountAuthCert(accountId uint64, authCertTagType entity.TagType, tagPath ...string) []*entity.ResourceAuthCert
// FillAuthCert 填充资源的授权凭证信息
// FillAuthCertByAcs 根据授权凭证列表填充资源的授权凭证信息
// @param authCerts 授权凭证列表
// @param resources 实现了entity.IAuthCert接口的资源信息
FillAuthCert(authCerts []*entity.ResourceAuthCert, resources ...entity.IAuthCert)
FillAuthCertByAcs(authCerts []*entity.ResourceAuthCert, resources ...entity.IAuthCert)
// FillAuthCert 填充资源对应的授权凭证信息
// @param resourceType 资源类型
// @param resources 实现了entity.IAuthCert接口的资源信息
FillAuthCert(resourceType int8, resources ...entity.IAuthCert)
}
type resourceAuthCertAppImpl struct {
@@ -60,11 +69,7 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
resourceCode := params.ResourceCode
resourceType := int8(params.ResourceType)
resourceAuthCerts := params.AuthCerts
authCertTagType := getResourceAuthCertTagType(entity.TagType(resourceType))
if authCertTagType == 0 {
return errorx.NewBiz("资源授权凭证所属标签类型不能为空")
}
if resourceCode == "" {
return errorx.NewBiz("资源授权凭证的资源编号不能为空")
}
@@ -79,15 +84,6 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
return err
}
// 删除该资源下的所有授权凭证资源标签
if err := r.tagTreeApp.DeleteResource(ctx, &DelResourceTagParam{
ResourceCode: resourceCode,
ResourceType: params.ResourceType,
ChildType: authCertTagType,
}); err != nil {
return err
}
return nil
}
@@ -98,7 +94,7 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
name2AuthCert[resourceAuthCert.Name] = resourceAuthCert
existNameAc := &entity.ResourceAuthCert{Name: resourceAuthCert.Name}
if r.GetBy(existNameAc) == nil && existNameAc.ResourceCode != resourceCode {
if resourceAuthCert.Id == 0 && r.GetBy(existNameAc) == nil && existNameAc.ResourceCode != resourceCode {
return errorx.NewBiz("授权凭证的名称不能重复[%s]", resourceAuthCert.Name)
}
@@ -141,29 +137,6 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
if err := r.BatchInsert(ctx, addAuthCerts); err != nil {
return err
}
// 获取资源编号对应的资源标签信息
var resourceTags []*entity.TagTree
r.tagTreeApp.ListByCond(&entity.TagTree{Type: params.ResourceType, Code: resourceCode}, &resourceTags)
// 资源标签id相当于父tag id
resourceTagIds := collx.ArrayMap(resourceTags, func(tag *entity.TagTree) uint64 {
return tag.Id
})
if len(resourceTagIds) > 0 {
// 保存授权凭证类型的资源标签
for _, authCert := range addAuthCerts {
logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-授权凭证标签[%d-%s]关联至所属资源标签下[%v]", resourceType, resourceCode, authCertTagType, authCert.Name, resourceTagIds)
if err := r.tagTreeApp.SaveResource(ctx, &SaveResourceTagParam{
ResourceCode: authCert.Name,
ResourceType: authCertTagType,
ResourceName: authCert.Username,
TagIds: resourceTagIds,
}); err != nil {
return err
}
}
}
}
for _, del := range dels {
@@ -171,13 +144,6 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
if err := r.DeleteByCond(ctx, &entity.ResourceAuthCert{ResourceCode: resourceCode, ResourceType: resourceType, Name: del}); err != nil {
return err
}
// 删除对应授权凭证资源标签
if err := r.tagTreeApp.DeleteResource(ctx, &DelResourceTagParam{
ResourceCode: del,
ResourceType: authCertTagType,
}); err != nil {
return err
}
}
if len(unmodifys) > 0 {
@@ -185,6 +151,7 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
oldName2AuthCert := collx.ArrayToMap(oldAuthCert, func(ac *entity.ResourceAuthCert) string {
return ac.Name
})
acTagType := GetResourceAuthCertTagType(params.ResourceType)
for _, unmodify := range unmodifys {
unmodifyAc := name2AuthCert[unmodify]
if unmodifyAc.Id == 0 {
@@ -197,13 +164,11 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
continue
}
logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-更新授权凭证-[%v]", resourceType, resourceCode, unmodify)
if oldAuthCert.Username != unmodifyAc.Username {
if err := r.updateAuthCertTagName(ctx, unmodify, authCertTagType, unmodifyAc.Username); err != nil {
logx.WarnfContext(ctx, "授权凭证[%s]修改了用户名-同步更新授权凭证标签名失败", unmodify)
}
// 如果修改了用户名且该凭证关联至标签则需要更新对应的标签名资源授权凭证类型的标签名为username
if oldAuthCert.Username != unmodifyAc.Username && acTagType != 0 {
r.updateAuthCertTagName(ctx, unmodify, acTagType, unmodifyAc.Username)
}
logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-更新授权凭证-[%v]", resourceType, resourceCode, unmodify)
if err := r.UpdateById(ctx, unmodifyAc); err != nil {
return err
}
@@ -213,6 +178,17 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
return nil
}
func (r *resourceAuthCertAppImpl) RelateAuthCert2ResourceTag(ctx context.Context, param *RelateAuthCertParam) error {
return r.tagTreeApp.RelateTagsByCodeAndType(ctx, &RelateTagsByCodeAndTypeParam{
ParentTagCode: param.ResourceCode,
ParentTagType: param.ResourceType,
TagType: GetResourceAuthCertTagType(param.ResourceType),
Tags: collx.ArrayMap(param.AuthCerts, func(rac *entity.ResourceAuthCert) ITag {
return rac
}),
})
}
func (r *resourceAuthCertAppImpl) SaveAuthCert(ctx context.Context, rac *entity.ResourceAuthCert) error {
if rac.Id == 0 {
return r.addAuthCert(ctx, rac)
@@ -242,7 +218,7 @@ func (r *resourceAuthCertAppImpl) DeleteAuthCert(ctx context.Context, id uint64)
return r.Tx(ctx,
func(ctx context.Context) error {
// 删除对应授权凭证标签
return r.tagTreeApp.DeleteResource(ctx, &DelResourceTagParam{
return r.tagTreeApp.DeleteTagByParam(ctx, &DelResourceTagParam{
ResourceCode: rac.Name,
})
},
@@ -288,7 +264,7 @@ func (r *resourceAuthCertAppImpl) GetAccountAuthCert(accountId uint64, authCertT
Type: authCertTagType,
CodePathLikes: tagPath,
}
authCertTags := r.tagTreeApp.GetAccountTagResources(accountId, tagQuery)
authCertTags := r.tagTreeApp.GetAccountTags(accountId, tagQuery)
// 获取所有授权凭证名称
authCertNames := collx.ArrayMap(authCertTags, func(tag *entity.TagTree) string {
@@ -303,7 +279,7 @@ func (r *resourceAuthCertAppImpl) GetAccountAuthCert(accountId uint64, authCertT
return authCerts
}
func (r *resourceAuthCertAppImpl) FillAuthCert(authCerts []*entity.ResourceAuthCert, resources ...entity.IAuthCert) {
func (r *resourceAuthCertAppImpl) FillAuthCertByAcs(authCerts []*entity.ResourceAuthCert, resources ...entity.IAuthCert) {
if len(resources) == 0 || len(authCerts) == 0 {
return
}
@@ -329,6 +305,19 @@ func (r *resourceAuthCertAppImpl) FillAuthCert(authCerts []*entity.ResourceAuthC
}
}
func (r *resourceAuthCertAppImpl) FillAuthCert(resourceType int8, resources ...entity.IAuthCert) {
if len(resources) == 0 {
return
}
resourceCodes := collx.ArrayMap(resources, func(ac entity.IAuthCert) string {
return ac.GetCode()
})
var acs []*entity.ResourceAuthCert
r.Repo.ListByWheres(collx.M{"resource_code in ?": resourceCodes, "resource_type = ?": resourceType}, &acs)
r.FillAuthCertByAcs(acs, resources...)
}
// addAuthCert 添加授权凭证
func (r *resourceAuthCertAppImpl) addAuthCert(ctx context.Context, rac *entity.ResourceAuthCert) error {
if r.CountByCond(&entity.ResourceAuthCert{Name: rac.Name}) > 0 {
@@ -344,32 +333,45 @@ func (r *resourceAuthCertAppImpl) addAuthCert(ctx context.Context, rac *entity.R
resourceCode := rac.ResourceCode
resourceType := rac.ResourceType
// 资源对应的授权凭证标签类型若为0则说明该资源不需要关联至资源tagTree
authCertTagType := GetResourceAuthCertTagType(entity.TagType(resourceType))
// 获取资源编号对应的资源标签信息
var resourceTags []*entity.TagTree
r.tagTreeApp.ListByCond(&entity.TagTree{Type: entity.TagType(resourceType), Code: resourceCode}, &resourceTags)
// 资源标签id相当于父tag id
resourceTagIds := collx.ArrayMap(resourceTags, func(tag *entity.TagTree) uint64 {
return tag.Id
})
if len(resourceTagIds) == 0 {
return errorx.NewBiz("资源标签不存在[%s], 请检查资源编号是否正确", resourceCode)
var resourceTagIds []uint64
// 如果该资源存在对应的授权凭证标签类型则说明需要关联至tagTree否则直接从授权凭证库中验证资源编号是否正确即可一个资源最少有一个授权凭证
if authCertTagType != 0 {
// 获取资源编号对应的资源标签信息
var resourceTags []*entity.TagTree
r.tagTreeApp.ListByCond(&entity.TagTree{Type: entity.TagType(resourceType), Code: resourceCode}, &resourceTags)
// 资源标签id相当于父tag id
resourceTagIds = collx.ArrayMap(resourceTags, func(tag *entity.TagTree) uint64 {
return tag.Id
})
if len(resourceTagIds) == 0 {
return errorx.NewBiz("资源标签不存在[%s], 请检查资源编号是否正确", resourceCode)
}
} else {
if r.CountByCond(&entity.ResourceAuthCert{ResourceCode: resourceCode, ResourceType: resourceType}) == 0 {
return errorx.NewBiz("该授权凭证关联的资源信息不存在, 请检查资源编号")
}
}
authCertTagType := getResourceAuthCertTagType(entity.TagType(resourceType))
// 如果密文类型不为公共凭证,则进行加密。公共凭证密文内容存的是明文的公共凭证名
if rac.CiphertextType != entity.AuthCertCiphertextTypePublic {
rac.CiphertextEncrypt()
}
return r.Tx(ctx, func(ctx context.Context) error {
logx.DebugfContext(ctx, "[%d-%s]-授权凭证标签[%d-%s]关联至所属资源标签下[%v]", resourceType, resourceCode, authCertTagType, rac.Name, resourceTagIds)
return r.tagTreeApp.SaveResource(ctx, &SaveResourceTagParam{
ResourceCode: rac.Name,
ResourceType: authCertTagType,
ResourceName: rac.Username,
TagIds: resourceTagIds,
})
// 若存在需要关联到的资源标签,则关联到对应的资源标签下
if len(resourceTagIds) > 0 {
logx.DebugfContext(ctx, "[%d-%s]-授权凭证标签[%d-%s]关联至所属资源标签下[%v]", resourceType, resourceCode, authCertTagType, rac.Name, resourceTagIds)
return r.tagTreeApp.SaveResourceTag(ctx, &SaveResourceTagParam{
Code: rac.Name,
Type: GetResourceAuthCertTagType(entity.TagType(resourceType)),
Name: rac.Username,
ParentTagIds: resourceTagIds,
})
}
return nil
}, func(ctx context.Context) error {
return r.Insert(ctx, rac)
})
@@ -401,9 +403,12 @@ func (r *resourceAuthCertAppImpl) updateAuthCert(ctx context.Context, rac *entit
}
// 修改了用户名,则需要同步更新对应授权凭证标签里的名称
if rac.Username != oldRac.Username {
if err := r.updateAuthCertTagName(ctx, oldRac.Name, getResourceAuthCertTagType(entity.TagType(oldRac.ResourceType)), rac.Username); err != nil {
return errorx.NewBiz("同步更新授权凭证标签名称失败")
if rac.Username != oldRac.Username && rac.ResourceType == int8(entity.TagTypeMachine) {
authCertTagType := GetResourceAuthCertTagType(entity.TagType(oldRac.ResourceType))
if authCertTagType != 0 {
if err := r.updateAuthCertTagName(ctx, oldRac.Name, authCertTagType, rac.Username); err != nil {
return errorx.NewBiz("同步更新授权凭证标签名称失败")
}
}
}
}
@@ -420,18 +425,6 @@ func (r *resourceAuthCertAppImpl) updateAuthCertTagName(ctx context.Context, aut
return r.tagTreeApp.UpdateByWheres(ctx, &entity.TagTree{Name: newTagName}, collx.M{"code = ?": authCertName, "type = ?": autyCertTagType})
}
// getResourceAuthCertTagType 根据资源类型,获取对应的授权凭证标签类型
func getResourceAuthCertTagType(resourceType entity.TagType) entity.TagType {
if resourceType == entity.TagTypeMachine {
return entity.TagTypeMachineAuthCert
}
if resourceType == entity.TagTypeDb {
return entity.TagTypeDbAuthCert
}
return -2
}
// 解密授权凭证信息
func (r *resourceAuthCertAppImpl) decryptAuthCert(authCert *entity.ResourceAuthCert) (*entity.ResourceAuthCert, error) {
if authCert.CiphertextType == entity.AuthCertCiphertextTypePublic {
@@ -457,3 +450,13 @@ func (r *resourceAuthCertAppImpl) decryptAuthCert(authCert *entity.ResourceAuthC
}
return authCert, nil
}
// GetResourceAuthCertTagType 根据资源类型获取对应的授权凭证标签类型return 0 说明该资源授权凭证不关联至tagTree
func GetResourceAuthCertTagType(resourceType entity.TagType) entity.TagType {
if resourceType == entity.TagTypeMachine {
return entity.TagTypeMachineAuthCert
}
// 该资源不存在对应的授权凭证标签即tag_tree不关联该资源的授权凭证
return 0
}

View File

@@ -2,26 +2,46 @@ package application
import (
"context"
"fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/collx"
"strings"
)
// 保存资源标签参数
type SaveResourceTagParam struct {
ResourceCode string
ResourceName string
ResourceType entity.TagType
// 标签接口,实现了该接口的结构体默认都可以当成标签树的一种标签
type ITag interface {
// 资源标签code
GetCode() string
TagIds []uint64 // 关联标签,相当于父标签 pid空数组则为删除该资源绑定的标签
// 资源标签
GetName() string
}
// 保存标签参数
type SaveResourceTagParam struct {
Code string
Name string
Type entity.TagType
ParentTagIds []uint64 // 关联标签,空数组则为删除该资源绑定的标签
}
type RelateTagsByCodeAndTypeParam struct {
ParentTagCode string // 父标签编号
ParentTagType entity.TagType // 父标签类型
TagType entity.TagType // 要关联的标签类型
Tags []ITag // 要关联的标签数组
}
type DelResourceTagParam struct {
Id uint64
ResourceCode string
ResourceType entity.TagType
@@ -40,23 +60,25 @@ type TagTree interface {
Delete(ctx context.Context, id uint64) error
// 获取指定账号有权限操作的资源信息列表
// 获取指定账号有权限操作的标签列表
// @param accountId 账号id
// @param resourceType 资源类型
// @param tagPath 访问指定的标签路径下关联的资源
GetAccountTagResources(accountId uint64, query *entity.TagTreeQuery) []*entity.TagTree
// @param query 查询条件
GetAccountTags(accountId uint64, query *entity.TagTreeQuery) []*entity.TagTree
// 获取指定账号有权限操作的资源codes
GetAccountResourceCodes(accountId uint64, resourceType int8, tagPath string) []string
// 获取指定账号有权限操作的标签codes
GetAccountTagCodes(accountId uint64, resourceType int8, tagPath string) []string
// SaveResource 保存资源标签
SaveResource(ctx context.Context, req *SaveResourceTagParam) error
// SaveResourceTag 保存资源类型标签
SaveResourceTag(ctx context.Context, param *SaveResourceTagParam) error
// DeleteResource 删除资源标签,会删除该资源下所有子节点信息
DeleteResource(ctx context.Context, param *DelResourceTagParam) error
// RelateTagsByCodeAndType 将指定标签数组关联至满足指定标签类型和标签code的标签下
RelateTagsByCodeAndType(ctx context.Context, param *RelateTagsByCodeAndTypeParam) error
// 根据资源信息获取对应的标签路径列表
ListTagPathByResource(resourceType int8, resourceCode string) []string
// DeleteTagByParam 删除标签,会删除该标签下所有子标签信息以及团队关联的标签信息
DeleteTagByParam(ctx context.Context, param *DelResourceTagParam) error
// 根据标签类型和标签code获取对应的标签路径列表
ListTagPathByTypeAndCode(resourceType int8, resourceCode string) []string
// 根据账号id获取其可访问标签信息
ListTagByAccountId(accountId uint64) []string
@@ -65,7 +87,7 @@ type TagTree interface {
CanAccess(accountId uint64, tagPath ...string) error
// 填充资源的标签信息
FillTagInfo(resources ...entity.ITagResource)
FillTagInfo(resourceTagType entity.TagType, resources ...entity.ITagResource)
}
type tagTreeAppImpl struct {
@@ -114,7 +136,7 @@ func (p *tagTreeAppImpl) Save(ctx context.Context, tag *entity.TagTree) error {
}
// 普通标签类型
tag.Type = -1
tag.Type = entity.TagTypeTag
return p.Insert(ctx, tag)
}
@@ -128,7 +150,7 @@ func (p *tagTreeAppImpl) ListByQuery(condition *entity.TagTreeQuery, toEntity an
p.GetRepo().SelectByCondition(condition, toEntity)
}
func (p *tagTreeAppImpl) GetAccountTagResources(accountId uint64, query *entity.TagTreeQuery) []*entity.TagTree {
func (p *tagTreeAppImpl) GetAccountTags(accountId uint64, query *entity.TagTreeQuery) []*entity.TagTree {
tagResourceQuery := &entity.TagTreeQuery{
Type: query.Type,
}
@@ -182,8 +204,8 @@ func (p *tagTreeAppImpl) GetAccountTagResources(accountId uint64, query *entity.
return tagResources
}
func (p *tagTreeAppImpl) GetAccountResourceCodes(accountId uint64, resourceType int8, tagPath string) []string {
tagResources := p.GetAccountTagResources(accountId, &entity.TagTreeQuery{Type: entity.TagType(resourceType), CodePathLikes: []string{tagPath}})
func (p *tagTreeAppImpl) GetAccountTagCodes(accountId uint64, resourceType int8, tagPath string) []string {
tagResources := p.GetAccountTags(accountId, &entity.TagTreeQuery{Type: entity.TagType(resourceType), CodePathLikes: []string{tagPath}})
// resouce code去重
code2Resource := collx.ArrayToMap[*entity.TagTree, string](tagResources, func(val *entity.TagTree) string {
return val.Code
@@ -192,34 +214,34 @@ func (p *tagTreeAppImpl) GetAccountResourceCodes(accountId uint64, resourceType
return collx.MapKeys(code2Resource)
}
func (p *tagTreeAppImpl) SaveResource(ctx context.Context, req *SaveResourceTagParam) error {
resourceCode := req.ResourceCode
resourceType := entity.TagType(req.ResourceType)
resourceName := req.ResourceName
tagIds := req.TagIds
func (p *tagTreeAppImpl) SaveResourceTag(ctx context.Context, param *SaveResourceTagParam) error {
code := param.Code
tagType := entity.TagType(param.Type)
name := param.Name
tagIds := param.ParentTagIds
if resourceCode == "" {
if code == "" {
return errorx.NewBiz("资源编号不能为空")
}
if resourceType == 0 {
if tagType == 0 {
return errorx.NewBiz("资源类型不能为空")
}
// 如果tagIds为空数组则为删除该资源标签
if len(tagIds) == 0 {
return p.DeleteResource(ctx, &DelResourceTagParam{
ResourceType: resourceType,
ResourceCode: resourceCode,
return p.DeleteTagByParam(ctx, &DelResourceTagParam{
ResourceType: tagType,
ResourceCode: code,
})
}
if resourceName == "" {
resourceName = resourceCode
if name == "" {
name = code
}
// 该资源对应的旧资源标签信息
var oldTagTree []*entity.TagTree
p.ListByCond(&entity.TagTree{Type: resourceType, Code: resourceCode}, &oldTagTree)
p.ListByCond(&entity.TagTree{Type: tagType, Code: code}, &oldTagTree)
var addTagIds, delTagIds []uint64
if len(oldTagTree) == 0 {
@@ -240,11 +262,12 @@ func (p *tagTreeAppImpl) SaveResource(ctx context.Context, req *SaveResourceTagP
}
addTagResource = append(addTagResource, &entity.TagTree{
Pid: tagId,
Code: resourceCode,
Type: resourceType,
Name: resourceName,
CodePath: tag.CodePath + resourceCode + entity.CodePathSeparator,
Code: code,
Type: tagType,
Name: name,
CodePath: fmt.Sprintf("%s%d%s%s%s", tag.CodePath, tagType, entity.CodePathResourceSeparator, code, entity.CodePathSeparator), // tag1/tag2/1|resourceCode1/11|resourceCode2
})
}
if err := p.BatchInsert(ctx, addTagResource); err != nil {
return err
@@ -253,9 +276,9 @@ func (p *tagTreeAppImpl) SaveResource(ctx context.Context, req *SaveResourceTagP
if len(delTagIds) > 0 {
for _, tagId := range delTagIds {
if err := p.DeleteResource(ctx, &DelResourceTagParam{
ResourceType: resourceType,
ResourceCode: resourceCode,
if err := p.DeleteTagByParam(ctx, &DelResourceTagParam{
ResourceType: tagType,
ResourceCode: code,
Pid: tagId,
}); err != nil {
return err
@@ -267,29 +290,136 @@ func (p *tagTreeAppImpl) SaveResource(ctx context.Context, req *SaveResourceTagP
return nil
}
func (p *tagTreeAppImpl) DeleteResource(ctx context.Context, param *DelResourceTagParam) error {
// 获取资源编号对应的资源标签信息
var resourceTags []*entity.TagTree
p.ListByCond(&entity.TagTree{Type: param.ResourceType, Code: param.ResourceCode, Pid: param.Pid}, &resourceTags)
if len(resourceTags) == 0 {
return nil
func (p *tagTreeAppImpl) RelateTagsByCodeAndType(ctx context.Context, param *RelateTagsByCodeAndTypeParam) error {
parentTagCode := param.ParentTagCode
parentTagType := param.ParentTagType
tagType := param.TagType
// 如果资源为,则表示清楚关联
if len(param.Tags) == 0 {
// 删除该资源下的所有指定类型的资源
return p.DeleteTagByParam(ctx, &DelResourceTagParam{
ResourceCode: parentTagCode,
ResourceType: param.ParentTagType,
ChildType: tagType,
})
}
delTagType := param.ChildType
for _, resourceTag := range resourceTags {
// 删除所有code_path下的子标签
if err := p.DeleteByWheres(ctx, collx.M{
"code_path LIKE ?": resourceTag.CodePath + "%",
"type = ?": delTagType,
}); err != nil {
// 获取满足指定编号与类型的所有标签信息
var parentTags []*entity.TagTree
p.ListByCond(&entity.TagTree{Type: parentTagType, Code: parentTagCode}, &parentTags)
// 标签id相当于需要关联的标签数组的父tag id
parentTagIds := collx.ArrayMap(parentTags, func(tag *entity.TagTree) uint64 {
return tag.Id
})
if len(parentTagIds) == 0 {
return errorx.NewBiz("不存在满足[type=%d, code=%s]的标签", parentTagType, parentTagCode)
}
var oldChildrenTags []*entity.TagTree
// 获取该资源的所有旧的该类型的子标签
p.ListByQuery(&entity.TagTreeQuery{
CodePathLikes: collx.ArrayMap[*entity.TagTree, string](parentTags, func(val *entity.TagTree) string { return val.CodePath }),
Type: tagType,
}, &oldChildrenTags)
// 组合新的授权凭证资源标签
newTags := make([]*entity.TagTree, 0)
for _, resourceTag := range parentTags {
for _, resource := range param.Tags {
tagCode := resource.GetCode()
newTags = append(newTags, &entity.TagTree{
Pid: resourceTag.Id,
Type: tagType,
Code: tagCode,
CodePath: fmt.Sprintf("%s%d%s%s%s", resourceTag.CodePath, tagType, entity.CodePathResourceSeparator, tagCode, entity.CodePathSeparator),
Name: resource.GetName(),
})
}
}
// 旧的codePath -> tag
oldCodePath2Tag := collx.ArrayToMap[*entity.TagTree, string](oldChildrenTags, func(val *entity.TagTree) string { return val.CodePath })
// 新的codePath -> tag
newCodePath2Tag := collx.ArrayToMap[*entity.TagTree, string](newTags, func(val *entity.TagTree) string { return val.CodePath })
var addCodePaths, delCodePaths []string
addCodePaths, delCodePaths, _ = collx.ArrayCompare(collx.MapKeys(newCodePath2Tag), collx.MapKeys(oldCodePath2Tag))
if len(addCodePaths) > 0 {
logx.DebugfContext(ctx, "RelateTags2CodeAndType[%d-%s]-新增标签[%v]", parentTagType, parentTagCode, addCodePaths)
addTags := make([]*entity.TagTree, 0)
for _, addCodePath := range addCodePaths {
addTags = append(addTags, newCodePath2Tag[addCodePath])
}
if err := p.BatchInsert(ctx, addTags); err != nil {
return err
}
}
if len(delCodePaths) > 0 {
logx.DebugfContext(ctx, "RelateTags2CodeAndType[%d-%s]-删除标签[%v]", parentTagType, parentTagCode, delCodePaths)
for _, delCodePath := range delCodePaths {
oldTag := oldCodePath2Tag[delCodePath]
if oldTag == nil {
continue
}
if err := p.DeleteTagByParam(ctx, &DelResourceTagParam{
Id: oldTag.Id,
}); err != nil {
return err
}
}
}
return nil
}
func (p *tagTreeAppImpl) ListTagPathByResource(resourceType int8, resourceCode string) []string {
func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *DelResourceTagParam) error {
// 获取资源编号对应的资源标签信息
var resourceTags []*entity.TagTree
cond := &entity.TagTree{Type: param.ResourceType, Code: param.ResourceCode, Pid: param.Pid}
cond.Id = param.Id
p.ListByCond(cond, &resourceTags)
if len(resourceTags) == 0 {
logx.DebugfContext(ctx, "TagTreeApp.DeleteResource[%d-%s]不存在可删除的标签", param.ResourceType, param.ResourceCode)
return nil
}
delTagType := param.ChildType
for _, resourceTag := range resourceTags {
// 获取所有关联的子标签
var childrenTag []*entity.TagTree
p.Repo.ListByWheres(collx.M{
"code_path LIKE ?": resourceTag.CodePath + "%",
"type = ?": delTagType,
}, &childrenTag)
if len(childrenTag) == 0 {
continue
}
childrenTagIds := collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 {
return item.Id
})
// 删除所有code_path下的子标签
if err := p.DeleteByWheres(ctx, collx.M{
"id in ?": childrenTagIds,
}); err != nil {
return err
}
// 删除team关联的标签
return p.tagTreeTeamRepo.DeleteByWheres(ctx, collx.M{"tag_id in ?": childrenTagIds})
}
return nil
}
func (p *tagTreeAppImpl) ListTagPathByTypeAndCode(resourceType int8, resourceCode string) []string {
var trs []*entity.TagTree
p.ListByCond(&entity.TagTree{Type: entity.TagType(resourceType), Code: resourceCode}, &trs)
return collx.ArrayMap(trs, func(tr *entity.TagTree) string {
@@ -318,7 +448,7 @@ func (p *tagTreeAppImpl) CanAccess(accountId uint64, tagPath ...string) error {
return errorx.NewBiz("您无权操作该资源")
}
func (p *tagTreeAppImpl) FillTagInfo(resources ...entity.ITagResource) {
func (p *tagTreeAppImpl) FillTagInfo(resourceTagType entity.TagType, resources ...entity.ITagResource) {
if len(resources) == 0 {
return
}
@@ -330,11 +460,11 @@ func (p *tagTreeAppImpl) FillTagInfo(resources ...entity.ITagResource) {
// 获取所有资源code关联的标签列表信息
var tagResources []*entity.TagTree
p.ListByQuery(&entity.TagTreeQuery{Codes: collx.MapKeys(resourceCode2Resouce)}, &tagResources)
p.ListByQuery(&entity.TagTreeQuery{Codes: collx.MapKeys(resourceCode2Resouce), Type: resourceTagType}, &tagResources)
for _, tr := range tagResources {
// 赋值标签信息
resourceCode2Resouce[tr.Code].SetTagInfo(entity.ResourceTag{TagId: tr.Pid, TagPath: tr.GetParentPath(0)})
resourceCode2Resouce[tr.Code].SetTagInfo(entity.ResourceTag{TagId: tr.Pid, TagPath: tr.GetTagPath()})
}
}

View File

@@ -97,6 +97,16 @@ func (m *ResourceAuthCert) GetExtraString(key string) string {
return cast.ToString(m.Extra[key])
}
// IResourceTag接口 授权凭证名 -> 资源标签code
func (m *ResourceAuthCert) GetCode() string {
return m.Name
}
// IResourceTag接口 授权凭证用户名 -> 资源标签名
func (m *ResourceAuthCert) GetName() string {
return m.Username
}
// HasChanged 与指定授权凭证比较是否有变更
func (m *ResourceAuthCert) HasChanged(rac *ResourceAuthCert) bool {
if rac == nil {

View File

@@ -23,17 +23,18 @@ type TagType int8
const (
// 标识路径分隔符
CodePathSeparator = "/"
// 标签路径资源段分隔符
CodePathResourceSeparator = "|"
TagTypeTag TagType = -1
TagTypeMachine TagType = TagType(consts.TagResourceTypeMachine)
TagTypeDb TagType = TagType(consts.TagResourceTypeDb)
TagTypeRedis TagType = TagType(consts.TagResourceTypeRedis)
TagTypeMongo TagType = TagType(consts.TagResourceTypeMongo)
TagTypeMachine TagType = TagType(consts.ResourceTypeMachine)
TagTypeDb TagType = TagType(consts.ResourceTypeDb)
TagTypeRedis TagType = TagType(consts.ResourceTypeRedis)
TagTypeMongo TagType = TagType(consts.ResourceTypeMongo)
// ----- (单独声明各个资源的授权凭证类型而不统一使用一个授权凭证类型是为了获取登录账号的授权凭证标签(ResourceAuthCertApp.GetAccountAuthCert)时,避免查出所有资源的授权凭证)
TagTypeMachineAuthCert TagType = 11 // 机器-授权凭证
TagTypeDbAuthCert TagType = 21 // DB-授权凭证
)
// GetRootCode 获取根路径信息
@@ -62,6 +63,30 @@ func (pt *TagTree) GetParentPath(index int) string {
return parentPath + "/"
}
// GetTagPath 获取标签段路径,不获取对应资源相关路径
func (pt *TagTree) GetTagPath() string {
codePath := pt.CodePath
// 以 资源分隔符"|" 符号对字符串进行分割
parts := strings.Split(codePath, CodePathResourceSeparator)
if len(parts) < 2 {
return codePath
}
// 从分割后的第一个子串中提取所需部分
substringBeforeNumber := parts[0]
// 找到最后一个 "/" 的位置
lastSlashIndex := strings.LastIndex(substringBeforeNumber, CodePathSeparator)
// 如果找到最后一个 "/" 符号,则截取子串
if lastSlashIndex != -1 {
return substringBeforeNumber[:lastSlashIndex+1]
}
return codePath
}
// 标签接口资源,如果要实现资源结构体填充标签信息,则资源结构体需要实现该接口
type ITagResource interface {
// 获取资源code

View File

@@ -18,6 +18,7 @@ var (
func RegisterCustomPatterns() {
// 账号用户名校验
RegisterPattern("account_username", "^[a-zA-Z0-9_]{5,20}$", "只允许输入5-20位大小写字母、数字、下划线")
RegisterPattern("resource_code", "^[a-zA-Z0-9_.:]{1,32}$", "只允许输入1-32位大小写字母、数字、_.:")
}
// 注册自定义正则表达式

View File

@@ -873,7 +873,7 @@ CREATE TABLE `t_tag_tree` (
`pid` bigint(20) NOT NULL DEFAULT '0',
`type` tinyint NOT NULL DEFAULT '-1' COMMENT '类型: -1.普通标签; 其他值则为对应的资源类型',
`code` varchar(36) NOT NULL COMMENT '标识符',
`code_path` varchar(255) NOT NULL COMMENT '标识符路径',
`code_path` varchar(555) NOT NULL COMMENT '标识符路径',
`name` varchar(36) DEFAULT NULL COMMENT '名称',
`remark` varchar(255) DEFAULT NULL,
`create_time` datetime NOT NULL,

View File

@@ -76,7 +76,7 @@ INSERT
select
tag_id,
resource_code,
CONCAT(tag_path , resource_code, '/'),
CONCAT(tag_path ,resource_type , '|', resource_code, '/'),
resource_type,
resource_code,
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
@@ -175,7 +175,7 @@ INSERT
SELECT
tt.id,
rac.`name`,
CONCAT(tt.code_path, rac.`name`, '/'),
CONCAT(tt.code_path, '11|' ,rac.`name`, '/'),
11,
rac.`username`,
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
@@ -188,9 +188,9 @@ SELECT
FROM
`t_tag_tree` tt
JOIN `t_resource_auth_cert` rac ON tt.`code` = rac.`resource_code`
AND tt.`type` = rac.`resource_type`
AND tt.`type` = rac.`resource_type` AND rac.type = 1
WHERE
tt.`is_deleted` = 0
tt.`is_deleted` = 0;
-- 删除机器表 账号相关字段
ALTER TABLE t_machine DROP COLUMN username;
@@ -205,3 +205,65 @@ INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1712717337, 1712717290, 'tLb8TKLB/m2abQkA8/', 2, 1, '授权凭证密文查看', 'authcert:showciphertext', 1712717337, 'null', 1, 'admin', 1, 'admin', '2024-04-10 10:48:58', '2024-04-10 10:48:58', 0, NULL);
commit;
-- 关联数据库账号至授权凭证表
begin;
ALTER TABLE t_db_instance ADD code varchar(36) NULL COMMENT '唯一编号';
ALTER TABLE t_db_instance CHANGE code code varchar(36) NULL COMMENT '唯一编号' AFTER id;
UPDATE t_db_instance SET code = CONCAT('db_code_', id);
INSERT
INTO
t_resource_auth_cert (name,
resource_code,
resource_type,
type,
username,
ciphertext,
ciphertext_type,
create_time,
creator_id,
creator,
update_time,
modifier_id,
modifier,
is_deleted)
select
CONCAT(code, '_', username),
code,
2,
1,
username,
password,
1,
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
1,
'admin',
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
1,
'admin',
0
from
t_db_instance
WHERE
is_deleted = 0;
ALTER TABLE t_db ADD auth_cert_name varchar(36) NULL COMMENT '授权凭证名';
ALTER TABLE t_db CHANGE auth_cert_name auth_cert_name varchar(36) NULL COMMENT '授权凭证名' AFTER instance_id;
UPDATE
t_db d
SET
d.auth_cert_name = (
SELECT
rac.name
FROM
t_resource_auth_cert rac
join t_db_instance di on
rac.resource_code = di.code
and rac.resource_type = 2
WHERE
di.id = d.instance_id);
ALTER TABLE t_tag_tree MODIFY COLUMN code_path varchar(555) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '标识符路径';
commit;