refactor: 初步提交全局授权凭证-资源多账号改造

This commit is contained in:
meilin.huang
2024-04-09 12:55:51 +08:00
parent 408bac09a1
commit 21498584b1
59 changed files with 1779 additions and 656 deletions

View File

@@ -56,7 +56,7 @@
"prettier": "^3.2.5",
"sass": "^1.69.0",
"typescript": "^5.3.2",
"vite": "^5.2.6",
"vite": "^5.2.8",
"vue-eslint-parser": "^9.4.2"
},
"browserslist": [

View File

@@ -7,4 +7,5 @@ export const TagResourceTypeEnum = {
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' }),
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket' }),
};

View File

@@ -43,7 +43,7 @@ const clipboardRef = ref({} as any);
const props = defineProps({
machineId: {
type: Number,
type: [Number, String],
required: true,
},
clipboardList: {

View File

@@ -0,0 +1,28 @@
<template>
<div v-if="props.authCerts">
<el-select default-first-option value-key="name" style="width: 100%" v-model="selectAuthCert" size="small">
<el-option v-for="item in props.authCerts" :key="item.name" :label="item.username" :value="item">
{{ item.username }}
<el-divider direction="vertical" border-style="dashed" />
<EnumTag :value="item.type" :enums="AuthCertTypeEnum" />
<el-divider direction="vertical" border-style="dashed" />
<EnumTag :value="item.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
</el-option>
</el-select>
</div>
</template>
<script lang="ts" setup>
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
const props = defineProps({
authCerts: {
type: [Array<any>],
required: true,
},
});
const selectAuthCert = defineModel('selectAuthCert');
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,172 @@
<template>
<div class="auth-cert-manage">
<el-table :data="authCerts" max-height="180" stripe style="width: 100%" size="small">
<el-table-column min-wdith="120px">
<template #header>
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="edit(null)"> </el-button>
</template>
<template #default="scope">
<el-link @click="edit(scope.row)" type="primary" icon="edit"></el-link>
<el-link class="ml5" v-auth="'machine:file:del'" type="danger" @click="deleteRow(scope.$index)" icon="delete"></el-link>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="100px"> </el-table-column>
<el-table-column prop="username" label="用户名" min-width="120px" show-overflow-tooltip> </el-table-column>
<el-table-column prop="ciphertextType" label="密文类型" width="100px">
<template #default="scope">
<EnumTag :value="scope.row.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
</template>
</el-table-column>
<el-table-column prop="type" label="凭证类型" width="100px">
<template #default="scope">
<EnumTag :value="scope.row.type" :enums="AuthCertTypeEnum" />
</template>
</el-table-column>
</el-table>
<el-dialog title="凭证保存" v-model="state.dvisible" :show-close="false" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
<el-form ref="acForm" :model="state.form" label-width="auto">
<el-form-item prop="type" label="凭证类型" required>
<el-select style="width: 100%" v-model="form.type" placeholder="请选择凭证类型">
<el-option v-for="item in AuthCertTypeEnum" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="ciphertextType" label="密文类型" required>
<el-select style="width: 100%" v-model="form.ciphertextType" placeholder="请选择密文类型">
<el-option v-for="item in AuthCertCiphertextTypeEnum" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input :disabled="form.id" v-model="form.name"></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item v-if="form.ciphertextType == AuthCertCiphertextTypeEnum.Password.value" prop="ciphertext" label="密码">
<el-input type="password" show-password clearable v-model.trim="form.ciphertext" placeholder="请输入密码" autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item v-if="form.ciphertextType == AuthCertCiphertextTypeEnum.PrivateKey.value" prop="ciphertext" label="秘钥">
<el-input type="textarea" :rows="5" v-model="form.ciphertext" placeholder="请将私钥文件内容拷贝至此"> </el-input>
</el-form-item>
<el-form-item v-if="form.ciphertextType == AuthCertCiphertextTypeEnum.PrivateKey.value" prop="passphrase" label="秘钥密码">
<el-input type="password" v-model="form.extra.passphrase"> </el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelEdit"> </el-button>
<el-button type="primary" :loading="btnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, toRefs } from 'vue';
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
import { resourceAuthCertApi } from '../tag/api';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
resourceType: { type: Number },
resourceCode: { type: String },
});
const authCerts = defineModel<any>('modelValue', { required: true, default: [] });
const acForm: any = ref(null);
const DefaultForm = {
id: null,
name: '',
username: '',
ciphertextType: AuthCertCiphertextTypeEnum.Password.value,
type: AuthCertTypeEnum.Private.value,
ciphertext: '',
extra: {} as any,
remark: '',
};
const state = reactive({
dvisible: false,
params: [] as any,
form: { ...DefaultForm },
btnLoading: false,
edit: false,
});
const { form, btnLoading } = toRefs(state);
onMounted(() => {
getAuthCerts();
});
const getAuthCerts = async () => {
if (!props.resourceCode || !props.resourceType) {
return;
}
const res = await resourceAuthCertApi.listByQuery.request({
resourceCode: props.resourceCode,
resourceType: props.resourceType,
pageNum: 1,
pageSize: 100,
});
authCerts.value = res.list?.reverse() || [];
};
const edit = (form: any) => {
if (form) {
state.form = form;
state.edit = true;
}
state.dvisible = true;
};
const deleteRow = (idx: any) => {
authCerts.value.splice(idx, 1);
};
const cancelEdit = () => {
state.dvisible = false;
setTimeout(() => {
state.form = { ...DefaultForm };
}, 300);
};
const btnOk = async () => {
acForm.value.validate(async (valid: boolean) => {
if (valid) {
const isEdit = state.form.id;
if (isEdit || state.edit) {
cancelEdit();
return;
}
if (authCerts.value?.filter((x: any) => x.username == state.form.username || x.name == state.form.name).length > 0) {
ElMessage.error('该名称或用户名已存在于该账号列表中');
return;
}
const res = await resourceAuthCertApi.listByQuery.request({
name: state.form.name,
pageNum: 1,
pageSize: 100,
});
if (res.total) {
ElMessage.error('该授权凭证名称已存在');
return;
}
authCerts.value.push(state.form);
cancelEdit();
}
});
};
</script>
<style lang="scss"></style>

View File

@@ -48,12 +48,7 @@
</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-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>

View File

@@ -1,87 +1,65 @@
<template>
<div>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false">
<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="machineForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item ref="tagSelectRef" prop="tagId" label="标签">
<tag-tree-select
multiple
@change-tag="
(tagIds) => {
form.tagId = tagIds;
tagSelectRef.validate();
}
"
:tag-path="form.tagPath"
:select-tags="form.tagId"
style="width: 100%"
/>
</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>
<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="protocol" label="协议" required>
<el-radio-group v-model="form.protocol" @change="handleChangeProtocol">
<el-radio v-for="item in MachineProtocolEnum" :key="item.value" :label="item.label" :value="item.value"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="ip" label="ip" required>
<el-col :span="18">
<el-input v-model.trim="form.ip" 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-divider content-position="left">基本</el-divider>
<el-form-item ref="tagSelectRef" prop="tagId" label="标签">
<tag-tree-select
multiple
@change-tag="
(tagIds) => {
form.tagId = tagIds;
tagSelectRef.validate();
}
"
:tag-path="form.tagPath"
:select-tags="form.tagId"
style="width: 100%"
/>
</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>
<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="protocol" label="协议" required>
<el-radio-group v-model="form.protocol" @change="handleChangeProtocol">
<el-radio v-for="item in MachineProtocolEnum" :key="item.value" :label="item.label" :value="item.value"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="ip" label="ip" required>
<el-col :span="18">
<el-input v-model.trim="form.ip" 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 prop="username" label="用户名">
<el-input v-model.trim="form.username" placeholder="请输授权用户名" autocomplete="new-password"> </el-input>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item>
<el-form-item label="认证方式" required>
<el-select @change="changeAuthMethod" style="width: 100%" v-model="state.authType" placeholder="请选认证方式">
<el-option key="1" label="密码" :value="1"> </el-option>
<el-option key="2" label="授权凭证" :value="2"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="state.authType == 1" prop="password" label="密码">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
</el-input>
</el-form-item>
<el-divider content-position="left">账号</el-divider>
<div>
<ResourceAuthCertEdit v-model="form.authCerts" :resource-code="form.code" :resource-type="TagResourceTypeEnum.Machine.value" />
</div>
<el-form-item v-if="state.authType == 2" prop="authCertId" label="授权凭证" required>
<auth-cert-select ref="authCertSelectRef" v-model="form.authCertId" />
</el-form-item>
<!-- <el-tab-pane label="其他配置" name="other"> -->
<el-divider content-position="left">其他</el-divider>
<el-form-item prop="enableRecorder" label="终端回放">
<el-checkbox v-model="form.enableRecorder" :true-value="1" :false-value="-1"></el-checkbox>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="enableRecorder" label="终端回放">
<el-checkbox v-model="form.enableRecorder" :true-value="1" :false-value="-1"></el-checkbox>
</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>
@@ -100,11 +78,12 @@ import { reactive, ref, toRefs, watch } from 'vue';
import { machineApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import ResourceAuthCertEdit from '../component/ResourceAuthCertEdit.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import AuthCertSelect from './authcert/AuthCertSelect.vue';
import { MachineProtocolEnum } from './enums';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { ResourceCodePattern } from '@/common/pattern';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({
visible: {
@@ -162,24 +141,9 @@ const rules = {
trigger: ['blur'],
},
],
authCertId: [
{
required: true,
message: '请选择授权凭证',
trigger: ['change', 'blur'],
},
],
username: [
{
required: true,
message: '请输入授权用户名',
trigger: ['change', 'blur'],
},
],
};
const machineForm: any = ref(null);
const authCertSelectRef: any = ref(null);
const tagSelectRef: any = ref(null);
const defaultForm = {
@@ -188,11 +152,9 @@ const defaultForm = {
tagPath: '',
ip: null,
port: 22,
protocol: 1, // 1.ssh 2.rdp
protocol: MachineProtocolEnum.Ssh.value,
name: null,
authCertId: null as any,
username: '',
password: '',
authCerts: [],
tagId: [],
remark: '',
sshTunnelMachineId: null as any,
@@ -201,16 +163,13 @@ const defaultForm = {
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
sshTunnelMachineList: [] as any,
authCerts: [] as any,
authType: 1,
form: defaultForm,
submitForm: {},
pwd: '',
});
const { dialogVisible, tabActiveName, form, submitForm } = toRefs(state);
const { dialogVisible, form, submitForm } = toRefs(state);
const { isFetching: testConnBtnLoading, execute: testConnExec } = machineApi.testConn.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveMachineExec } = machineApi.saveMachine.useApi(submitForm);
@@ -221,32 +180,13 @@ watch(props, async (newValue: any) => {
state.form = defaultForm;
return;
}
state.tabActiveName = 'basic';
if (newValue.machine) {
state.form = { ...newValue.machine };
state.form.tagId = newValue.machine.tags.map((t: any) => t.tagId);
// 如果凭证类型为公共的,则表示使用授权凭证认证
const authCertId = (state.form as any).authCertId;
if (authCertId > 0) {
state.authType = 2;
} else {
state.authType = 1;
}
} else {
state.authType = 1;
state.form.authCerts = newValue.machine.authCerts || [];
}
});
const changeAuthMethod = (val: any) => {
if (state.form.id) {
if (val == 2) {
state.form.authCertId = null;
} else {
state.form.password = '';
}
}
};
const testConn = async () => {
machineForm.value.validate(async (valid: boolean) => {
if (!valid) {
@@ -267,6 +207,11 @@ const btnOk = async () => {
return false;
}
if (state.form.authCerts.length == 0) {
ElMessage.error('请完善授权凭证账号信息');
return false;
}
state.submitForm = getReqForm();
await saveMachineExec();
ElMessage.success('保存成功');
@@ -277,10 +222,6 @@ const btnOk = async () => {
const getReqForm = () => {
const reqForm: any = { ...state.form };
// 如果为密码认证则置空授权凭证id
if (state.authType == 1) {
reqForm.authCertId = -1;
}
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1;
}

View File

@@ -4,6 +4,7 @@
ref="pageTableRef"
:page-api="machineApi.list"
:before-query-fn="checkRouteTagPath"
:data-handler-fn="handleData"
:search-items="searchItems"
v-model:query-form="params"
:show-selection="true"
@@ -84,13 +85,17 @@
<ResourceTags :tags="data.tags" />
</template>
<template #authCert="{ data }">
<ResourceAuthCert v-model:select-auth-cert="data.selectAuthCert" :auth-certs="data.authCerts" />
</template>
<template #action="{ data }">
<span v-auth="'machine:terminal'">
<el-tooltip v-if="data.protocol == 1" :show-after="500" content="按住ctrl则为新标签打开" placement="top">
<el-tooltip v-if="data.protocol == MachineProtocolEnum.Ssh.value" :show-after="500" content="按住ctrl则为新标签打开" placement="top">
<el-button :disabled="data.status == -1" type="primary" @click="showTerminal(data, $event)" link>SSH</el-button>
</el-tooltip>
<el-button v-if="data.protocol == 2" type="primary" @click="showRDP(data)" link>RDP</el-button>
<el-button v-if="data.protocol == MachineProtocolEnum.Rdp.value" type="primary" @click="showRDP(data)" link>RDP</el-button>
<el-button v-if="data.protocol == 3" type="primary" @click="showRDP(data)" link>VNC</el-button>
<el-divider direction="vertical" border-style="dashed" />
@@ -140,11 +145,6 @@
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</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.authCertId > 1 ? '授权凭证' : '密码' }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
@@ -162,7 +162,7 @@
<template #headerTitle="{ terminalInfo }">
{{ `${(terminalInfo.terminalId + '').slice(-2)}` }}
<el-divider direction="vertical" />
{{ `${terminalInfo.meta.username}@${terminalInfo.meta.ip}:${terminalInfo.meta.port}` }}
{{ `${terminalInfo.meta.selectAuthCert.username}@${terminalInfo.meta.ip}:${terminalInfo.meta.port}` }}
<el-divider direction="vertical" />
{{ terminalInfo.meta.name }}
</template>
@@ -179,7 +179,12 @@
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
<file-conf-list
:title="fileDialog.title"
v-model:visible="fileDialog.visible"
v-model:machineId="fileDialog.machineId"
:auth-cert-name="fileDialog.authCertName"
/>
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title"></machine-stats>
@@ -221,6 +226,8 @@ import { TagResourceTypeEnum } from '@/common/commonEnum';
import { SearchItem } from '@/components/SearchForm';
import { getTagPathSearchItem } from '../component/tag';
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
import { MachineProtocolEnum } from './enums';
// 组件
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
@@ -265,7 +272,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('username', '用户名'),
TableColumn.new('authCerts[0].username', '授权凭证').isSlot('authCert').setAddWidth(20),
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
TableColumn.new('remark', '备注'),
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
@@ -300,6 +307,7 @@ const state = reactive({
fileDialog: {
visible: false,
machineId: 0,
authCertName: '',
title: '',
},
filesystemDialog: {
@@ -308,6 +316,7 @@ const state = reactive({
protocol: 1,
title: '',
fileId: 0,
authCertName: '',
path: '',
},
machineStatsDialog: {
@@ -360,6 +369,15 @@ const checkRouteTagPath = (query: any) => {
return query;
};
const handleData = (res: any) => {
const dataList = res.list;
// 赋值授权凭证
for (let x of dataList) {
x.selectAuthCert = x.authCerts[0];
}
return res;
};
const handleCommand = (commond: any) => {
const data = commond.data;
const type = commond.type;
@@ -392,12 +410,13 @@ const handleCommand = (commond: any) => {
};
const showTerminal = (row: any, event: PointerEvent) => {
const ac = row.selectAuthCert.name;
// 按住ctrl点击则新建标签页打开, metaKey对应mac command键
if (event.ctrlKey || event.metaKey) {
const { href } = router.resolve({
path: `/machine/terminal`,
query: {
id: row.id,
ac,
name: row.name,
},
});
@@ -408,9 +427,9 @@ const showTerminal = (row: any, event: PointerEvent) => {
const terminalId = Date.now();
terminalDialogRef.value.open({
terminalId,
socketUrl: getMachineTerminalSocketUrl(row.id),
socketUrl: getMachineTerminalSocketUrl(ac),
minTitle: `${row.name} [${(terminalId + '').slice(-2)}]`, // 截取terminalId最后两位区分多个terminal
minDesc: `${row.username}@${row.ip}:${row.port} (${row.name})`,
minDesc: `${row.selectAuthCert.username}@${row.ip}:${row.port} (${row.name})`,
meta: row,
});
};
@@ -485,18 +504,20 @@ const submitSuccess = () => {
};
const showFileManage = (data: any) => {
if (data.protocol === 1) {
if (data.protocol === MachineProtocolEnum.Ssh.value) {
// ssh
state.fileDialog.visible = true;
state.fileDialog.machineId = data.id;
state.fileDialog.title = `${data.name} => ${data.ip}`;
} else if (data.protocol === 2) {
state.fileDialog.authCertName = data.selectAuthCert.name;
state.fileDialog.title = `${data.name} => ${data.selectAuthCert.username}@${data.ip}`;
} else if (data.protocol === MachineProtocolEnum.Rdp.value) {
// rdp
state.filesystemDialog.protocol = 2;
state.filesystemDialog.machineId = data.id;
state.filesystemDialog.fileId = data.id;
state.filesystemDialog.authCertName = data.selectAuthCert.name;
state.filesystemDialog.path = '/';
state.filesystemDialog.title = `${data.name} => 远程桌面文件`;
state.filesystemDialog.title = `${data.name} => ${data.selectAuthCert.username}@远程桌面文件`;
state.filesystemDialog.visible = true;
}
};
@@ -534,7 +555,7 @@ const showRDP = (row: any, blank = false) => {
const { href } = router.resolve({
path: `/machine/terminal-rdp`,
query: {
id: row.id,
ac: row.selectAuthCert.name,
name: row.name,
},
});

View File

@@ -6,22 +6,26 @@
<tag-tree
class="machine-terminal-tree"
ref="tagTreeRef"
:resource-type="TagResourceTypeEnum.Machine.value"
:resource-type="TagResourceTypeEnum.MachineAuthCert.value"
:tag-path-node-type="NodeTypeTagPath"
>
<template #prefix="{ data }">
<SvgIcon v-if="data.icon && data.params.status == 1 && data.params.protocol == 1" :name="data.icon.name" :color="data.icon.color" />
<SvgIcon
v-if="data.icon && data.params.status == -1 && data.params.protocol == 1"
v-if="data.icon && data.params.status == 1 && data.params.protocol == MachineProtocolEnum.Ssh.value"
:name="data.icon.name"
:color="data.icon.color"
/>
<SvgIcon
v-if="data.icon && data.params.status == -1 && data.params.protocol == MachineProtocolEnum.Ssh.value"
:name="data.icon.name"
color="var(--el-color-danger)"
/>
<SvgIcon v-if="data.icon && data.params.protocol != 1" :name="data.icon.name" :color="data.icon.color" />
<SvgIcon v-if="data.icon && data.params.protocol != MachineProtocolEnum.Ssh.value" :name="data.icon.name" :color="data.icon.color" />
</template>
<template #suffix="{ data }">
<span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.Machine">{{
` ${data.params.username}@${data.params.ip}:${data.params.port}`
<span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.AuthCert">{{
` ${data.params.selectAuthCert.username}@${data.params.ip}:${data.params.port}`
}}</span>
</template>
</tag-tree>
@@ -66,14 +70,14 @@
<div :ref="(el: any) => setTerminalWrapperRef(el, dt.key)" class="terminal-wrapper" style="height: calc(100vh - 155px)">
<TerminalBody
v-if="dt.params.protocol == 1"
v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
:mount-init="false"
@status-change="terminalStatusChange(dt.key, $event)"
:ref="(el: any) => setTerminalRef(el, dt.key)"
:socket-url="dt.socketUrl"
/>
<machine-rdp
v-if="dt.params.protocol != 1"
v-if="dt.params.protocol != MachineProtocolEnum.Ssh.value"
:machine-id="dt.params.id"
:ref="(el: any) => setTerminalRef(el, dt.key)"
@status-change="terminalStatusChange(dt.key, $event)"
@@ -87,16 +91,11 @@
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</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.authCertId > 1 ? '授权凭证' : '密码' }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
@@ -160,6 +159,9 @@ import TerminalBody from '@/components/terminal/TerminalBody.vue';
import { TerminalStatus } from '@/components/terminal/common';
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
import ResourceTags from '../component/ResourceTags.vue';
import { MachineProtocolEnum } from './enums';
// 组件
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
@@ -182,6 +184,7 @@ const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
class MachineNodeType {
static Machine = 1;
static AuthCert = 2;
}
const state = reactive({
@@ -238,7 +241,9 @@ const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog
const tagTreeRef: any = ref(null);
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: any) => {
let openIds = {};
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
// 加载标签树下的机器列表
state.params.tagPath = node.key;
state.params.pageNum = 1;
@@ -247,60 +252,71 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
// 把list 根据name字段排序
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
return res.list.map((x: any) =>
new TagTreeNode(x.id, x.name, NodeTypeMachine(x))
new TagTreeNode(x.id, x.name, NodeTypeMachine)
.withParams(x)
.withDisabled(x.status == -1 && x.protocol == 1)
.withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
.withIcon({
name: 'Monitor',
color: '#409eff',
})
.withIsLeaf(true)
);
});
let openIds = {};
const NodeTypeMachine = new NodeType(MachineNodeType.Machine)
.withLoadNodesFunc((node: TagTreeNode) => {
const machine = node.params;
// 获取授权凭证列表
const authCerts = machine.authCerts;
return authCerts.map((x: any) =>
new TagTreeNode(x.id, x.username, NodeTypeAuthCert)
.withParams({ ...machine, selectAuthCert: x })
.withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value)
.withIcon({
name: 'Ticket',
color: '#409eff',
})
.withIsLeaf(true)
);
})
.withContextMenuItems([
new ContextmenuItem('detail', '详情').withIcon('More').withOnClick((node: any) => showInfo(node.params)),
new ContextmenuItem('status', '状态').withIcon('Compass').withOnClick((node: any) => showMachineStats(node.params)),
new ContextmenuItem('process', '进程').withIcon('DataLine').withOnClick((node: any) => showProcess(node.params)),
new ContextmenuItem('edit', '终端回放')
.withIcon('Compass')
.withOnClick((node: any) => showRec(node.params))
.withHideFunc((node: any) => actionBtns[perms.updateMachine] && node.params.enableRecorder == 1),
]);
const NodeTypeMachine = (machine: any) => {
let contextMenuItems = [];
contextMenuItems.push(new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick(() => openTerminal(machine)));
contextMenuItems.push(new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick(() => openTerminal(machine, true)));
contextMenuItems.push(new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick(() => showFileManage(machine)));
contextMenuItems.push(new ContextmenuItem('scripts', '脚本管理').withIcon('Files').withOnClick(() => serviceManager(machine)));
contextMenuItems.push(new ContextmenuItem('detail', '详情').withIcon('More').withOnClick(() => showInfo(machine)));
contextMenuItems.push(new ContextmenuItem('status', '状态').withIcon('Compass').withOnClick(() => showMachineStats(machine)));
contextMenuItems.push(new ContextmenuItem('process', '进程').withIcon('DataLine').withOnClick(() => showProcess(machine)));
if (actionBtns[perms.updateMachine] && machine.enableRecorder == 1) {
contextMenuItems.push(new ContextmenuItem('edit', '终端回放').withIcon('Compass').withOnClick(() => showRec(machine)));
}
return new NodeType(MachineNodeType.Machine).withContextMenuItems(contextMenuItems).withNodeDblclickFunc(() => {
// for (let k of state.tabs.keys()) {
// // 存在该机器相关的终端tab则直接激活该tab
// if (k.startsWith(`${machine.id}_${machine.username}_`)) {
// state.activeTermName = k;
// onTabChange();
// return;
// }
// }
openTerminal(machine);
});
};
const NodeTypeAuthCert = new NodeType(MachineNodeType.AuthCert)
.withNodeDblclickFunc((node: TagTreeNode) => {
openTerminal(node.params);
})
.withContextMenuItems([
new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick((node: any) => openTerminal(node.params)),
new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick((node: any) => openTerminal(node.params, true)),
new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick((node: any) => showFileManage(node.params)),
new ContextmenuItem('scripts', '脚本管理').withIcon('Files').withOnClick((node: any) => serviceManager(node.params)),
]);
const openTerminal = (machine: any, ex?: boolean) => {
// 授权凭证名
const ac = machine.selectAuthCert.name;
// 新窗口打开
if (ex) {
if (machine.protocol == 1) {
if (machine.protocol == MachineProtocolEnum.Ssh.value) {
const { href } = router.resolve({
path: `/machine/terminal`,
query: {
id: machine.id,
ac,
name: machine.name,
},
});
window.open(href, '_blank');
return;
} else if (machine.protocol == 2) {
}
if (machine.protocol == MachineProtocolEnum.Rdp.value) {
const { href } = router.resolve({
path: `/machine/terminal-rdp`,
query: {
@@ -313,21 +329,22 @@ const openTerminal = (machine: any, ex?: boolean) => {
}
}
let { name, id, username } = machine;
let { name, username } = machine;
const labelName = `${machine.selectAuthCert.username}@${name}`;
// 同一个机器的终端打开多次key后添加下划线和数字区分
openIds[id] = openIds[id] ? ++openIds[id] : 1;
let sameIndex = openIds[id];
openIds[ac] = openIds[ac] ? ++openIds[ac] : 1;
let sameIndex = openIds[ac];
let key = `${id}_${username}_${sameIndex}`;
// 只保留name的10个字,超出部分只保留前后4个字符,中间用省略号代替
let label = name.length > 10 ? name.slice(0, 4) + '...' + name.slice(-4) : name;
let key = `${ac}_${username}_${sameIndex}`;
// 只保留name的15个字,超出部分只保留前后10个字符,中间用省略号代替
const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName;
let tab = {
key,
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
params: machine,
socketUrl: getMachineTerminalSocketUrl(id),
socketUrl: getMachineTerminalSocketUrl(ac),
};
state.tabs.set(key, tab);

View File

@@ -1,6 +1,6 @@
<template>
<div class="terminal-wrapper" ref="terminalWrapperRef">
<machine-rdp ref="rdpRef" :machine-id="route.query.id" />
<machine-rdp ref="rdpRef" :machine-id="route.query.ac" />
</div>
</template>
@@ -17,7 +17,6 @@ const terminalWrapperRef = ref({} as any);
onMounted(() => {
let width = terminalWrapperRef.value.clientWidth;
let height = terminalWrapperRef.value.clientHeight;
console.log(width, height);
rdpRef.value?.init(width, height, false);
});
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="terminal-wrapper">
<TerminalBody :socket-url="getMachineTerminalSocketUrl(route.query.id)" />
<TerminalBody :socket-url="getMachineTerminalSocketUrl(route.query.ac)" />
</div>
</template>

View File

@@ -65,10 +65,10 @@ export const cronJobApi = {
execList: Api.newGet('/machine-cronjobs/execs'),
};
export function getMachineTerminalSocketUrl(machineId: any) {
return `${config.baseWsUrl}/machines/${machineId}/terminal?${joinClientParams()}`;
export function getMachineTerminalSocketUrl(authCertName: any) {
return `${config.baseWsUrl}/machines/terminal/${authCertName}?${joinClientParams()}`;
}
export function getMachineRdpSocketUrl(machineId: any) {
return `${config.baseWsUrl}/machines/${machineId}/rdp`;
export function getMachineRdpSocketUrl(authCertName: any) {
return `${config.baseWsUrl}/machines/rdp/${authCertName}`;
}

View File

@@ -44,13 +44,21 @@
</el-row>
<el-dialog destroy-on-close :title="fileDialog.title" v-model="fileDialog.visible" :close-on-click-modal="false" width="70%">
<machine-file :title="fileDialog.title" :machine-id="machineId" :file-id="fileDialog.fileId" :path="fileDialog.path" :protocol="protocol" />
<machine-file
:title="fileDialog.title"
:machine-id="machineId"
:auth-cert-name="props.authCertName"
:file-id="fileDialog.fileId"
:path="fileDialog.path"
:protocol="protocol"
/>
</el-dialog>
<machine-file-content
:title="fileContent.title"
v-model:visible="fileContent.contentVisible"
:machine-id="machineId"
:auth-cert-name="props.authCertName"
:file-id="fileContent.fileId"
:path="fileContent.path"
/>
@@ -70,6 +78,7 @@ const props = defineProps({
visible: { type: Boolean },
protocol: { type: Number, default: 1 },
machineId: { type: Number },
authCertName: { type: String },
title: { type: String },
});

View File

@@ -268,6 +268,7 @@
<machine-file-content
v-model:visible="fileContent.contentVisible"
:machine-id="machineId"
:auth-cert-name="props.authCertName"
:file-id="fileId"
:path="fileContent.path"
:protocol="protocol"
@@ -290,6 +291,7 @@ import { getMachineConfig } from '@/common/sysconfig';
const props = defineProps({
machineId: { type: Number },
authCertName: { type: String },
protocol: { type: Number, default: 1 },
fileId: { type: Number, default: 0 },
path: { type: String, default: '' },
@@ -419,7 +421,8 @@ const pasteFile = async () => {
await api.request({
machineId: props.machineId,
fileId: props.fileId,
path: cmFile.paths,
authCertName: props.authCertName,
paths: cmFile.paths,
toPath: state.nowPath,
protocol: props.protocol,
});
@@ -462,8 +465,9 @@ const fileRename = async (row: any) => {
try {
await machineApi.renameFile.request({
machineId: parseInt(props.machineId + ''),
authCertName: props.authCertName,
fileId: parseInt(props.fileId + ''),
oldname: state.nowPath + pathSep + state.renameFile.oldname,
path: state.nowPath + pathSep + state.renameFile.oldname,
newname: state.nowPath + pathSep + row.name,
protocol: props.protocol,
});
@@ -508,6 +512,7 @@ const lsFile = async (path: string) => {
const res = await machineApi.lsFile.request({
fileId: props.fileId,
machineId: props.machineId,
authCertName: props.authCertName,
protocol: props.protocol,
path,
});
@@ -574,6 +579,7 @@ const createFile = async () => {
const path = state.nowPath + pathSep + name;
await machineApi.createFile.request({
machineId: props.machineId,
authCertName: props.authCertName,
id: props.fileId,
protocol: props.protocol,
path,
@@ -607,8 +613,9 @@ const deleteFile = async (files: any) => {
state.loading = true;
await machineApi.rmFile.request({
fileId: props.fileId,
path: files.map((x: any) => x.path),
paths: files.map((x: any) => x.path),
machineId: props.machineId,
authCertName: props.authCertName,
protocol: props.protocol,
});
ElMessage.success('删除成功');
@@ -624,7 +631,7 @@ const downloadFile = (data: any) => {
const a = document.createElement('a');
a.setAttribute(
'href',
`${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/download?path=${data.path}&machineId=${props.machineId}&protocol=${props.protocol}&${joinClientParams()}`
`${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/download?path=${data.path}&machineId=${props.machineId}&authCertName=${props.authCertName}&protocol=${props.protocol}&${joinClientParams()}`
);
a.click();
};
@@ -638,6 +645,7 @@ function uploadFolder(e: any) {
// 把文件夹数据放到formData里面下面的files和paths字段根据接口来定
var form = new FormData();
form.append('basePath', state.nowPath);
form.append('authCertName', props.authCertName as any);
form.append('machineId', props.machineId as any);
form.append('protocol', props.protocol as any);
form.append('fileId', props.fileId as any);
@@ -693,6 +701,7 @@ const uploadFile = (content: any) => {
const path = state.nowPath;
params.append('file', content.file);
params.append('path', path);
params.append('authCertName', props.authCertName as any);
params.append('machineId', props.machineId as any);
params.append('protocol', props.protocol as any);
params.append('fileId', props.fileId as any);

View File

@@ -35,6 +35,7 @@ const props = defineProps({
protocol: { type: Number, default: 1 },
title: { type: String, default: '' },
machineId: { type: Number },
authCertName: { type: String },
fileId: { type: Number, default: 0 },
path: { type: String, default: '' },
});
@@ -64,6 +65,7 @@ const getFileContent = async () => {
fileId: props.fileId,
path,
machineId: props.machineId,
authCertName: props.authCertName,
protocol: props.protocol,
});
state.fileType = getFileType(path);
@@ -81,6 +83,7 @@ const updateContent = async () => {
id: props.fileId,
path: props.path,
machineId: props.machineId,
authCertName: props.authCertName,
protocol: props.protocol,
});
ElMessage.success('修改成功');

View File

@@ -21,7 +21,7 @@
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入机器编号 (数字字母下划线), 不可修改"
placeholder="请输入编号 (数字字母下划线), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>

View File

@@ -25,7 +25,7 @@
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入机器编号 (数字字母下划线), 不可修改"
placeholder="请输入编号 (数字字母下划线), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>

View File

@@ -1,7 +1,7 @@
<template>
<div class="tag-tree-list card">
<Splitpanes class="default-theme">
<Pane size="25" min-size="20" max-size="30">
<Pane size="30" min-size="25" max-size="35">
<div class="card pd5 mr5">
<el-input v-model="filterTag" clearable placeholder="关键字过滤(右击操作)" style="width: 200px; margin-right: 10px" />
<el-button
@@ -28,7 +28,6 @@
<el-scrollbar class="tag-tree-data">
<el-tree
ref="tagTreeRef"
class="none-select"
node-key="id"
highlight-current
:props="props"
@@ -166,7 +165,7 @@ const contextmenuAdd = new ContextmenuItem('addTag', '添加子标签')
.withPermission('tag:save')
.withHideFunc((data: any) => {
// 非标签类型不可添加子标签
return data.type != -1;
return data.type != TagResourceTypeEnum.Tag.value || (data.children && data.children?.[0].type != TagResourceTypeEnum.Tag.value);
})
.withOnClick((data: any) => showSaveTagDialog(data));
@@ -180,7 +179,7 @@ const contextmenuDel = new ContextmenuItem('delete', '删除')
.withPermission('tag:del')
.withHideFunc((data: any) => {
// 存在子标签,则不允许删除
return data.children || data.type != -1;
return data.children || data.type != TagResourceTypeEnum.Tag.value;
})
.withOnClick((data: any) => deleteTag(data));
@@ -339,15 +338,6 @@ const deleteTag = (data: any) => {
});
};
// const changeStatus = async (data: any, status: any) => {
// await resourceApi.changeStatus.request({
// id: data.id,
// status: status,
// });
// data.status = status;
// ElMessage.success((status === 1 ? '启用' : '禁用') + '成功!');
// };
// 节点被展开时触发的事件
const handleNodeExpand = (data: any, node: any) => {
const id: any = node.data.id;
@@ -393,14 +383,4 @@ const removeDeafultExpandId = (id: any) => {
min-width: 100%;
}
}
.none-select {
moz-user-select: -moz-none;
-moz-user-select: none;
-o-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>

View File

@@ -20,3 +20,7 @@ export const tagApi = {
getTeamTagIds: Api.newGet('/teams/{teamId}/tags'),
saveTeamTags: Api.newPost('/teams/{teamId}/tags'),
};
export const resourceAuthCertApi = {
listByQuery: Api.newGet('/auth-certs'),
};

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import (
"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"
@@ -87,7 +88,7 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db, tagIds ...u
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceCode: dbEntity.Code,
ResourceType: consts.TagResourceTypeDb,
ResourceType: tagentity.TagTypeDb,
TagIds: tagIds,
})
})
@@ -127,7 +128,7 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db, tagIds ...u
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceCode: old.Code,
ResourceType: consts.TagResourceTypeDb,
ResourceType: tagentity.TagTypeDb,
TagIds: tagIds,
})
})
@@ -154,7 +155,7 @@ func (d *dbAppImpl) Delete(ctx context.Context, id uint64) error {
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceCode: db.Code,
ResourceType: consts.TagResourceTypeDb,
ResourceType: tagentity.TagTypeDb,
})
})
}

View File

@@ -1,21 +1,27 @@
package api
import (
"mayfly-go/internal/common/consts"
"mayfly-go/internal/machine/application"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
)
type Dashbord struct {
TagTreeApp tagapp.TagTree `inject:""`
MachineApp application.Machine `inject:""`
ResourceAuthCertApp tagapp.ResourceAuthCert `inject:""`
MachineApp application.Machine `inject:""`
}
func (m *Dashbord) Dashbord(rc *req.Ctx) {
accountId := rc.GetLoginAccount().Id
machienNum := len(m.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMachine, ""))
machienAuthCerts := m.ResourceAuthCertApp.GetAccountAuthCert(accountId, tagentity.TagTypeMachineAuthCert)
machineCodes := collx.ArrayMap(machienAuthCerts, func(ac *tagentity.ResourceAuthCert) string {
return ac.ResourceCode
})
machienNum := len(collx.ArrayDeduplicate(machineCodes))
rc.ResData = collx.M{
"machineNum": machienNum,

View File

@@ -1,5 +1,7 @@
package form
import tagentity "mayfly-go/internal/tag/domain/entity"
type MachineForm struct {
Id uint64 `json:"id"`
Protocol int `json:"protocol" binding:"required"`
@@ -8,11 +10,8 @@ type MachineForm struct {
Ip string `json:"ip" binding:"required"` // IP地址
Port int `json:"port" binding:"required"` // 端口号
// 资产授权凭证信息列表
AuthCertId int `json:"authCertId"`
TagId []uint64 `json:"tagId" binding:"required"`
Username string `json:"username"`
Password string `json:"password"`
TagId []uint64 `json:"tagId" binding:"required"`
AuthCerts []*tagentity.ResourceAuthCert // 资产授权凭证信息列表
Remark string `json:"remark"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
@@ -24,14 +23,6 @@ type MachineRunForm struct {
Cmd string `json:"cmd" binding:"required"`
}
type MachineFileForm struct {
Id uint64 `json:"id"`
Name string `json:"name" binding:"required"`
MachineId uint64 `json:"machineId" binding:"required"`
Type int `json:"type" binding:"required"`
Path string `json:"path" binding:"required"`
}
type MachineScriptForm struct {
Id uint64 `json:"id"`
Name string `json:"name" binding:"required"`
@@ -42,39 +33,6 @@ type MachineScriptForm struct {
Script string `json:"script" binding:"required"`
}
type ServerFileOptionForm struct {
MachineId uint64 `form:"machineId"`
Protocol int `form:"protocol"`
Path string `form:"path"`
Type string `form:"type"`
Content string `form:"content"`
Id uint64 `form:"id"`
FileId uint64 `form:"fileId"`
}
type MachineFileUpdateForm struct {
Content string `json:"content" binding:"required"`
Id uint64 `json:"id" binding:"required"`
Path string `json:"path" binding:"required"`
}
type MachineFileOpForm struct {
Path []string `json:"path" binding:"required"`
ToPath string `json:"toPath"`
MachineId uint64 `json:"machineId" binding:"required"`
Protocol int `json:"protocol" binding:"required"`
FileId uint64 `json:"fileId" binding:"required"`
}
type MachineFileRename struct {
MachineId uint64 `json:"machineId" binding:"required"`
Protocol int `json:"protocol" binding:"required"`
FileId uint64 `json:"fileId" binding:"required"`
Oldname string `json:"oldname" binding:"required"`
Newname string `json:"newname" binding:"required"`
}
// 授权凭证
type AuthCertForm struct {
Id uint64 `json:"id"`

View File

@@ -0,0 +1,48 @@
package form
import "mayfly-go/internal/machine/application"
type MachineFileForm struct {
Id uint64 `json:"id"`
Name string `json:"name" binding:"required"`
MachineId uint64 `json:"machineId" binding:"required"`
Type int `json:"type" binding:"required"`
Path string `json:"path" binding:"required"`
}
type MachineFileUpdateForm struct {
Content string `json:"content" binding:"required"`
Id uint64 `json:"id" binding:"required"`
Path string `json:"path" binding:"required"`
}
type CreateFileForm struct {
*application.MachineFileOpParam
Type string `json:"type"`
}
type WriteFileContentForm struct {
*application.MachineFileOpParam
Content string `json:"content" binding:"required"`
}
type RemoveFileForm struct {
*application.MachineFileOpParam
Paths []string `json:"paths" binding:"required"`
}
type CopyFileForm struct {
*application.MachineFileOpParam
Paths []string `json:"paths" binding:"required"`
ToPath string `json:"toPath" binding:"required"`
}
type RenameForm struct {
*application.MachineFileOpParam
Newname string `json:"newname" binding:"required"`
}

View File

@@ -3,10 +3,6 @@ package api
import (
"encoding/base64"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/may-fly/cast"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/machine/api/form"
"mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/application"
@@ -29,24 +25,33 @@ import (
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/may-fly/cast"
)
type Machine struct {
MachineApp application.Machine `inject:""`
MachineTermOpApp application.MachineTermOp `inject:""`
TagApp tagapp.TagTree `inject:"TagTreeApp"`
MachineApp application.Machine `inject:""`
MachineTermOpApp application.MachineTermOp `inject:""`
TagApp tagapp.TagTree `inject:"TagTreeApp"`
ResourceAuthCertApp tagapp.ResourceAuthCert `inject:""`
}
func (m *Machine) Machines(rc *req.Ctx) {
condition, pageParam := req.BindQueryAndPage(rc, new(entity.MachineQuery))
// 不存在可访问标签id即没有可操作数据
codes := m.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeMachine, condition.TagPath)
if len(codes) == 0 {
authCerts := m.ResourceAuthCertApp.GetAccountAuthCert(rc.GetLoginAccount().Id, tagentity.TagTypeMachineAuthCert, condition.TagPath)
// 不存在可操作的授权凭证,即没有可操作数据
if len(authCerts) == 0 {
rc.ResData = model.EmptyPageResult[any]()
return
}
condition.Codes = codes
machineCodes := collx.ArrayMap(authCerts, func(ac *tagentity.ResourceAuthCert) string {
return ac.ResourceCode
})
condition.Codes = collx.ArrayDeduplicate(machineCodes)
var machinevos []*vo.MachineVO
res, err := m.MachineApp.GetMachineList(condition, pageParam, &machinevos)
@@ -61,6 +66,11 @@ func (m *Machine) Machines(rc *req.Ctx) {
return mvo
})...)
// 填充授权凭证信息
m.ResourceAuthCertApp.FillAuthCert(authCerts, collx.ArrayMap(machinevos, func(mvo *vo.MachineVO) tagentity.IAuthCert {
return mvo
})...)
for _, mv := range machinevos {
if machineStats, err := m.MachineApp.GetMachineStats(mv.Id); err == nil {
mv.Stat = collx.M{
@@ -85,16 +95,20 @@ func (m *Machine) SaveMachine(rc *req.Ctx) {
machineForm := new(form.MachineForm)
me := req.BindJsonAndCopyTo(rc, machineForm, new(entity.Machine))
machineForm.Password = "******"
rc.ReqParam = machineForm
biz.ErrIsNil(m.MachineApp.SaveMachine(rc.MetaCtx, me, machineForm.TagId...))
biz.ErrIsNil(m.MachineApp.SaveMachine(rc.MetaCtx, &application.SaveMachineParam{
Machine: me,
TagIds: machineForm.TagId,
AuthCerts: machineForm.AuthCerts,
}))
}
func (m *Machine) TestConn(rc *req.Ctx) {
me := req.BindJsonAndCopyTo(rc, new(form.MachineForm), new(entity.Machine))
machineForm := new(form.MachineForm)
me := req.BindJsonAndCopyTo(rc, machineForm, new(entity.Machine))
// 测试连接
biz.ErrIsNilAppendErr(m.MachineApp.TestConn(me), "该机器无法连接: %s")
biz.ErrIsNilAppendErr(m.MachineApp.TestConn(me, machineForm.AuthCerts[0]), "该机器无法连接: %s")
}
func (m *Machine) ChangeStatus(rc *req.Ctx) {
@@ -175,7 +189,7 @@ func (m *Machine) WsSSH(g *gin.Context) {
panic(errorx.NewBiz("\033[1;31m您没有权限操作该机器终端,请重新登录后再试~\033[0m"))
}
cli, err := m.MachineApp.NewCli(GetMachineId(rc))
cli, err := m.MachineApp.NewCli(GetMachineAc(rc))
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
defer cli.Close()
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
@@ -231,9 +245,9 @@ func (m *Machine) WsGuacamole(g *gin.Context) {
biz.ErrIsNil(err)
rc := req.NewCtxWithGin(g).WithRequiredPermission(req.NewPermission("machine:terminal"))
machineId := GetMachineId(rc)
ac := GetMachineAc(rc)
mi, err := m.MachineApp.ToMachineInfoById(machineId)
mi, err := m.MachineApp.ToMachineInfoByAc(ac)
if err != nil {
return
}
@@ -258,7 +272,7 @@ func (m *Machine) WsGuacamole(g *gin.Context) {
if mi.EnableRecorder == 1 {
// 操作记录 查看文档https://guacamole.apache.org/doc/gug/configuring-guacamole.html#graphical-recording
params["recording-path"] = fmt.Sprintf("/rdp-rec/%d", machineId)
params["recording-path"] = fmt.Sprintf("/rdp-rec/%s", ac)
params["create-recording-path"] = "true"
params["recording-include-keys"] = "true"
}
@@ -273,14 +287,14 @@ func (m *Machine) WsGuacamole(g *gin.Context) {
if query.Get("force") != "" {
// 判断是否强制连接,是的话,查询是否有正在连接的会话,有的话强制关闭
if cast.ToBool(query.Get("force")) {
tn := sessions.Get(machineId)
tn := sessions.Get(ac)
if tn != nil {
_ = tn.Close()
}
}
}
tunnel, err := guac.DoConnect(query, params, machineId)
tunnel, err := guac.DoConnect(query, params, ac)
if err != nil {
return
}
@@ -290,9 +304,9 @@ func (m *Machine) WsGuacamole(g *gin.Context) {
}
}()
sessions.Add(machineId, wsConn, g.Request, tunnel)
sessions.Add(ac, wsConn, g.Request, tunnel)
defer sessions.Delete(machineId, wsConn, g.Request, tunnel)
defer sessions.Delete(ac, wsConn, g.Request, tunnel)
writer := tunnel.AcquireWriter()
reader := tunnel.AcquireReader()
@@ -312,3 +326,9 @@ func GetMachineId(rc *req.Ctx) uint64 {
biz.IsTrue(machineId != 0, "machineId错误")
return uint64(machineId)
}
func GetMachineAc(rc *req.Ctx) string {
ac := rc.PathParam("ac")
biz.IsTrue(ac != "", "authCertName错误")
return ac
}

View File

@@ -23,9 +23,10 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/may-fly/cast"
)
type MachineFile struct {
@@ -62,8 +63,7 @@ func (m *MachineFile) DeleteFile(rc *req.Ctx) {
/*** sftp相关操作 */
func (m *MachineFile) CreateFile(rc *req.Ctx) {
opForm := req.BindJsonAndValid(rc, new(form.ServerFileOptionForm))
opForm := req.BindJsonAndValid(rc, new(form.CreateFileForm))
path := opForm.Path
attrs := collx.Kvs("path", path)
@@ -71,10 +71,10 @@ func (m *MachineFile) CreateFile(rc *req.Ctx) {
var err error
if opForm.Type == dir {
attrs["type"] = "目录"
mi, err = m.MachineFileApp.MkDir(opForm.FileId, opForm.Path, opForm)
mi, err = m.MachineFileApp.MkDir(opForm.MachineFileOpParam)
} else {
attrs["type"] = "文件"
mi, err = m.MachineFileApp.CreateFile(opForm.FileId, opForm.Path, opForm)
mi, err = m.MachineFileApp.CreateFile(opForm.MachineFileOpParam)
}
attrs["machine"] = mi
rc.ReqParam = attrs
@@ -82,7 +82,7 @@ func (m *MachineFile) CreateFile(rc *req.Ctx) {
}
func (m *MachineFile) ReadFileContent(rc *req.Ctx) {
opForm := req.BindQuery(rc, new(form.ServerFileOptionForm))
opForm := req.BindQuery(rc, new(application.MachineFileOpParam))
readPath := opForm.Path
// 特殊处理rdp文件
if opForm.Protocol == entity.MachineProtocolRdp {
@@ -96,7 +96,7 @@ func (m *MachineFile) ReadFileContent(rc *req.Ctx) {
return
}
sftpFile, mi, err := m.MachineFileApp.ReadFile(opForm.FileId, readPath)
sftpFile, mi, err := m.MachineFileApp.ReadFile(opForm)
rc.ReqParam = collx.Kvs("machine", mi, "path", readPath)
biz.ErrIsNilAppendErr(err, "打开文件失败: %s")
defer sftpFile.Close()
@@ -112,7 +112,7 @@ func (m *MachineFile) ReadFileContent(rc *req.Ctx) {
}
func (m *MachineFile) DownloadFile(rc *req.Ctx) {
opForm := req.BindQuery(rc, new(form.ServerFileOptionForm))
opForm := req.BindQuery(rc, new(application.MachineFileOpParam))
readPath := opForm.Path
@@ -131,7 +131,7 @@ func (m *MachineFile) DownloadFile(rc *req.Ctx) {
return
}
sftpFile, mi, err := m.MachineFileApp.ReadFile(opForm.FileId, readPath)
sftpFile, mi, err := m.MachineFileApp.ReadFile(opForm)
rc.ReqParam = collx.Kvs("machine", mi, "path", readPath)
biz.ErrIsNilAppendErr(err, "打开文件失败: %s")
defer sftpFile.Close()
@@ -140,11 +140,11 @@ func (m *MachineFile) DownloadFile(rc *req.Ctx) {
}
func (m *MachineFile) GetDirEntry(rc *req.Ctx) {
opForm := req.BindQuery(rc, new(form.ServerFileOptionForm))
opForm := req.BindQuery(rc, new(application.MachineFileOpParam))
readPath := opForm.Path
rc.ReqParam = fmt.Sprintf("path: %s", readPath)
fis, err := m.MachineFileApp.ReadDir(opForm.FileId, opForm)
fis, err := m.MachineFileApp.ReadDir(opForm)
biz.ErrIsNilAppendErr(err, "读取目录失败: %s")
fisVO := make([]vo.MachineFileInfo, 0)
@@ -173,34 +173,34 @@ func (m *MachineFile) GetDirEntry(rc *req.Ctx) {
}
func (m *MachineFile) GetDirSize(rc *req.Ctx) {
opForm := req.BindQuery(rc, new(form.ServerFileOptionForm))
opForm := req.BindQuery(rc, new(application.MachineFileOpParam))
size, err := m.MachineFileApp.GetDirSize(opForm.FileId, opForm)
size, err := m.MachineFileApp.GetDirSize(opForm)
biz.ErrIsNil(err)
rc.ResData = size
}
func (m *MachineFile) GetFileStat(rc *req.Ctx) {
opForm := req.BindQuery(rc, new(form.ServerFileOptionForm))
opForm := req.BindQuery(rc, new(application.MachineFileOpParam))
res, err := m.MachineFileApp.FileStat(opForm)
biz.ErrIsNil(err, res)
rc.ResData = res
}
func (m *MachineFile) WriteFileContent(rc *req.Ctx) {
opForm := req.BindQuery(rc, new(form.ServerFileOptionForm))
opForm := req.BindJsonAndValid(rc, new(form.WriteFileContentForm))
path := opForm.Path
mi, err := m.MachineFileApp.WriteFileContent(opForm.FileId, path, []byte(opForm.Content), opForm)
mi, err := m.MachineFileApp.WriteFileContent(opForm.MachineFileOpParam, []byte(opForm.Content))
rc.ReqParam = collx.Kvs("machine", mi, "path", path)
biz.ErrIsNilAppendErr(err, "打开文件失败: %s")
}
func (m *MachineFile) UploadFile(rc *req.Ctx) {
fid := GetMachineFileId(rc)
path := rc.PostForm("path")
protocol, err := strconv.Atoi(rc.PostForm("protocol"))
machineId, err := strconv.Atoi(rc.PostForm("machineId"))
protocol := cast.ToInt(rc.PostForm("protocol"))
machineId := cast.ToUint64(rc.PostForm("machineId"))
authCertName := rc.PostForm("authCertName")
fileheader, err := rc.FormFile("file")
biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
@@ -219,14 +219,14 @@ func (m *MachineFile) UploadFile(rc *req.Ctx) {
}
}()
opForm := &form.ServerFileOptionForm{
FileId: fid,
MachineId: uint64(machineId),
Protocol: protocol,
Path: path,
opForm := &application.MachineFileOpParam{
MachineId: machineId,
AuthCertName: authCertName,
Protocol: protocol,
Path: path,
}
mi, err := m.MachineFileApp.UploadFile(fid, path, fileheader.Filename, file, opForm)
mi, err := m.MachineFileApp.UploadFile(opForm, fileheader.Filename, file)
rc.ReqParam = collx.Kvs("machine", mi, "path", fmt.Sprintf("%s/%s", path, fileheader.Filename))
biz.ErrIsNilAppendErr(err, "创建文件失败: %s")
// 保存消息并发送文件上传成功通知
@@ -239,8 +239,6 @@ type FolderFile struct {
}
func (m *MachineFile) UploadFolder(rc *req.Ctx) {
fid := GetMachineFileId(rc)
mf, err := rc.MultipartForm()
biz.ErrIsNilAppendErr(err, "获取表单信息失败: %s")
basePath := mf.Value["basePath"][0]
@@ -256,21 +254,24 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
biz.IsTrue(allFileSize <= maxUploadFileSize, "文件夹总大小不能超过%d字节", maxUploadFileSize)
paths := mf.Value["paths"]
authCertName := mf.Value["authCertName"][0]
machineId := cast.ToUint64(mf.Value["machineId"][0])
// protocol
protocol, err := strconv.Atoi(mf.Value["protocol"][0])
protocol := cast.ToInt(mf.Value["protocol"][0])
opForm := &application.MachineFileOpParam{
MachineId: machineId,
Protocol: protocol,
AuthCertName: authCertName,
}
if protocol == entity.MachineProtocolRdp {
machineId, _ := strconv.Atoi(mf.Value["machineId"][0])
opForm := &form.ServerFileOptionForm{
MachineId: uint64(machineId),
Protocol: protocol,
}
m.MachineFileApp.UploadFiles(basePath, fileheaders, paths, opForm)
m.MachineFileApp.UploadFiles(opForm, basePath, fileheaders, paths)
return
}
folderName := filepath.Dir(paths[0])
mcli, err := m.MachineFileApp.GetMachineCli(fid, basePath+"/"+folderName)
mcli, err := m.MachineFileApp.GetMachineCli(authCertName)
biz.ErrIsNil(err)
mi := mcli.Info
@@ -344,30 +345,30 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
}
func (m *MachineFile) RemoveFile(rc *req.Ctx) {
opForm := req.BindJsonAndValid(rc, new(form.MachineFileOpForm))
opForm := req.BindJsonAndValid(rc, new(form.RemoveFileForm))
mi, err := m.MachineFileApp.RemoveFile(opForm)
mi, err := m.MachineFileApp.RemoveFile(opForm.MachineFileOpParam, opForm.Paths...)
rc.ReqParam = collx.Kvs("machine", mi, "path", opForm)
biz.ErrIsNilAppendErr(err, "删除文件失败: %s")
}
func (m *MachineFile) CopyFile(rc *req.Ctx) {
opForm := req.BindJsonAndValid(rc, new(form.MachineFileOpForm))
mi, err := m.MachineFileApp.Copy(opForm)
opForm := req.BindJsonAndValid(rc, new(form.CopyFileForm))
mi, err := m.MachineFileApp.Copy(opForm.MachineFileOpParam, opForm.ToPath, opForm.Paths...)
biz.ErrIsNilAppendErr(err, "文件拷贝失败: %s")
rc.ReqParam = collx.Kvs("machine", mi, "cp", opForm)
}
func (m *MachineFile) MvFile(rc *req.Ctx) {
opForm := req.BindJsonAndValid(rc, new(form.MachineFileOpForm))
mi, err := m.MachineFileApp.Mv(opForm)
opForm := req.BindJsonAndValid(rc, new(form.CopyFileForm))
mi, err := m.MachineFileApp.Mv(opForm.MachineFileOpParam, opForm.ToPath, opForm.Paths...)
rc.ReqParam = collx.Kvs("machine", mi, "mv", opForm)
biz.ErrIsNilAppendErr(err, "文件移动失败: %s")
}
func (m *MachineFile) Rename(rc *req.Ctx) {
renameForm := req.BindJsonAndValid(rc, new(form.MachineFileRename))
mi, err := m.MachineFileApp.Rename(renameForm)
renameForm := req.BindJsonAndValid(rc, new(form.RenameForm))
mi, err := m.MachineFileApp.Rename(renameForm.MachineFileOpParam, renameForm.Newname)
rc.ReqParam = collx.Kvs("machine", mi, "rename", renameForm)
biz.ErrIsNilAppendErr(err, "文件重命名失败: %s")
}

View File

@@ -13,7 +13,8 @@ type AuthCertBaseVO struct {
}
type MachineVO struct {
tagentity.ResourceTags
tagentity.ResourceTags // 标签信息
tagentity.AuthCerts // 授权凭证信息
Id uint64 `json:"id"`
Code string `json:"code"`
@@ -21,8 +22,6 @@ type MachineVO struct {
Protocol int `json:"protocol"`
Ip string `json:"ip"`
Port int `json:"port"`
Username string `json:"username"`
AuthCertId int `json:"authCertId"`
Status *int8 `json:"status"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
CreateTime *time.Time `json:"createTime"`
@@ -33,8 +32,6 @@ type MachineVO struct {
ModifierId *int64 `json:"modifierId"`
Remark *string `json:"remark"`
EnableRecorder int8 `json:"enableRecorder"`
// TagId uint64 `json:"tagId"`
// TagPath string `json:"tagPath"`
Stat map[string]any `json:"stat" gorm:"-"`
}

View File

@@ -10,6 +10,7 @@ import (
"mayfly-go/internal/machine/infrastructure/cache"
"mayfly-go/internal/machine/mcm"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/global"
@@ -17,15 +18,23 @@ import (
"mayfly-go/pkg/model"
"mayfly-go/pkg/scheduler"
"time"
"github.com/may-fly/cast"
)
type SaveMachineParam struct {
Machine *entity.Machine
TagIds []uint64
AuthCerts []*tagentity.ResourceAuthCert
}
type Machine interface {
base.App[*entity.Machine]
SaveMachine(ctx context.Context, m *entity.Machine, tagIds ...uint64) error
SaveMachine(ctx context.Context, param *SaveMachineParam) error
// 测试机器连接
TestConn(me *entity.Machine) error
TestConn(me *entity.Machine, authCert *tagentity.ResourceAuthCert) error
// 调整机器状态
ChangeStatus(ctx context.Context, id uint64, status int8) error
@@ -36,11 +45,14 @@ type Machine interface {
GetMachineList(condition *entity.MachineQuery, pageParam *model.PageParam, toEntity *[]*vo.MachineVO, orderBy ...string) (*model.PageResult[*[]*vo.MachineVO], error)
// 新建机器客户端连接需手动调用Close
NewCli(id uint64) (*mcm.Cli, error)
NewCli(authCertName string) (*mcm.Cli, error)
// 获取已缓存的机器连接,若不存在则新建客户端连接并缓存,主要用于定时获取状态等(避免频繁创建连接)
GetCli(id uint64) (*mcm.Cli, error)
// 根据授权凭证获取客户端连接
GetCliByAc(authCertName string) (*mcm.Cli, error)
// 获取ssh隧道机器连接
GetSshTunnelMachine(id int) (*mcm.SshTunnelMachine, error)
@@ -50,14 +62,15 @@ type Machine interface {
// 获取机器运行时状态信息
GetMachineStats(machineId uint64) (*mcm.Stats, error)
ToMachineInfoById(machineId uint64) (*mcm.MachineInfo, error)
ToMachineInfoByAc(ac string) (*mcm.MachineInfo, error)
}
type machineAppImpl struct {
base.AppImpl[*entity.Machine, repository.Machine]
authCertApp AuthCert `inject:"AuthCertApp"`
tagApp tagapp.TagTree `inject:"TagTreeApp"`
// authCertApp AuthCert `inject:"AuthCertApp"`
tagApp tagapp.TagTree `inject:"TagTreeApp"`
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"ResourceAuthCertApp"`
}
// 注入MachineRepo
@@ -70,19 +83,21 @@ func (m *machineAppImpl) GetMachineList(condition *entity.MachineQuery, pagePara
return m.GetRepo().GetMachineList(condition, pageParam, toEntity, orderBy...)
}
func (m *machineAppImpl) SaveMachine(ctx context.Context, me *entity.Machine, tagIds ...uint64) error {
func (m *machineAppImpl) SaveMachine(ctx context.Context, param *SaveMachineParam) error {
me := param.Machine
tagIds := param.TagIds
authCerts := param.AuthCerts
resourceType := tagentity.TagTypeMachine
authCertTagType := tagentity.TagTypeMachineAuthCert
oldMachine := &entity.Machine{
Ip: me.Ip,
Port: me.Port,
Username: me.Username,
SshTunnelMachineId: me.SshTunnelMachineId,
}
err := m.GetBy(oldMachine)
if errEnc := me.PwdEncrypt(); errEnc != nil {
return errorx.NewBiz(errEnc.Error())
}
if me.Id == 0 {
if err == nil {
return errorx.NewBiz("该机器信息已存在")
@@ -94,14 +109,23 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, me *entity.Machine, ta
// 新增机器,默认启用状态
me.Status = entity.MachineStatusEnable
return m.Tx(ctx, func(ctx context.Context) error {
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{
ResourceCode: me.Code,
ResourceType: consts.TagResourceTypeMachine,
ResourceType: resourceType,
TagIds: tagIds,
})
}); err != nil {
return err
}
return m.resourceAuthCertApp.SaveAuthCert(ctx, &tagapp.SaveAuthCertParam{
ResourceCode: me.Code,
ResourceType: resourceType,
AuthCertTagType: authCertTagType,
AuthCerts: authCerts,
})
}
@@ -118,20 +142,29 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, me *entity.Machine, ta
mcm.DeleteCli(me.Id)
// 防止误传修改
me.Code = ""
return m.Tx(ctx, func(ctx context.Context) error {
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: consts.TagResourceTypeMachine,
ResourceType: resourceType,
TagIds: tagIds,
})
}); err != nil {
return err
}
return m.resourceAuthCertApp.SaveAuthCert(ctx, &tagapp.SaveAuthCertParam{
ResourceCode: oldMachine.Code,
ResourceType: resourceType,
AuthCertTagType: authCertTagType,
AuthCerts: authCerts,
})
}
func (m *machineAppImpl) TestConn(me *entity.Machine) error {
func (m *machineAppImpl) TestConn(me *entity.Machine, authCert *tagentity.ResourceAuthCert) error {
me.Id = 0
mi, err := m.toMachineInfo(me)
mi, err := m.toMi(me, authCert)
if err != nil {
return err
}
@@ -165,31 +198,51 @@ func (m *machineAppImpl) Delete(ctx context.Context, id uint64) error {
// 发布机器删除事件
global.EventBus.Publish(ctx, consts.DeleteMachineEventTopic, machine)
resourceType := tagentity.TagTypeMachine
return m.Tx(ctx,
func(ctx context.Context) error {
return m.DeleteById(ctx, id)
}, func(ctx context.Context) error {
return m.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceCode: machine.Code,
ResourceType: consts.TagResourceTypeMachine,
ResourceType: resourceType,
})
}, func(ctx context.Context) error {
return m.resourceAuthCertApp.SaveAuthCert(ctx, &tagapp.SaveAuthCertParam{
ResourceCode: machine.Code,
ResourceType: resourceType,
})
})
}
func (m *machineAppImpl) NewCli(machineId uint64) (*mcm.Cli, error) {
if mi, err := m.ToMachineInfoById(machineId); err != nil {
func (m *machineAppImpl) NewCli(authCertName string) (*mcm.Cli, error) {
if mi, err := m.ToMachineInfoByAc(authCertName); err != nil {
return nil, err
} else {
return mi.Conn()
}
}
func (m *machineAppImpl) GetCli(machineId uint64) (*mcm.Cli, error) {
return mcm.GetMachineCli(machineId, func(mid uint64) (*mcm.MachineInfo, error) {
return m.ToMachineInfoById(mid)
func (m *machineAppImpl) GetCliByAc(authCertName string) (*mcm.Cli, error) {
return mcm.GetMachineCli(authCertName, func(ac string) (*mcm.MachineInfo, error) {
return m.ToMachineInfoByAc(ac)
})
}
func (m *machineAppImpl) GetCli(machineId uint64) (*mcm.Cli, error) {
cli, err := mcm.GetMachineCliById(machineId)
if err == nil {
return cli, nil
}
_, authCert, err := m.getMachineAndAuthCert(machineId)
if err != nil {
return nil, err
}
return m.GetCliByAc(authCert.Name)
}
func (m *machineAppImpl) GetSshTunnelMachine(machineId int) (*mcm.SshTunnelMachine, error) {
return mcm.GetSshTunnelMachine(machineId, func(mid uint64) (*mcm.MachineInfo, error) {
return m.ToMachineInfoById(mid)
@@ -229,62 +282,68 @@ func (m *machineAppImpl) GetMachineStats(machineId uint64) (*mcm.Stats, error) {
return cache.GetMachineStats(machineId)
}
// 生成机器信息根据授权凭证id填充用户密码等
func (m *machineAppImpl) ToMachineInfoById(machineId uint64) (*mcm.MachineInfo, error) {
me, err := m.GetById(new(entity.Machine), machineId)
// 根据授权凭证,生成机器信息
func (m *machineAppImpl) ToMachineInfoByAc(authCertName string) (*mcm.MachineInfo, error) {
authCert, err := m.resourceAuthCertApp.GetAuthCert(authCertName)
if err != nil {
return nil, errorx.NewBiz("机器信息不存在")
}
if me.Status != entity.MachineStatusEnable && me.Protocol == 1 {
return nil, errorx.NewBiz("该机器已被停用")
return nil, err
}
if mi, err := m.toMachineInfo(me); err != nil {
return nil, err
} else {
return mi, nil
machine := &entity.Machine{
Code: authCert.ResourceCode,
}
if err := m.GetBy(machine); err != nil {
return nil, errorx.NewBiz("该授权凭证关联的机器信息不存在")
}
return m.toMi(machine, authCert)
}
func (m *machineAppImpl) toMachineInfo(me *entity.Machine) (*mcm.MachineInfo, error) {
// 生成机器信息根据授权凭证id填充用户密码等
func (m *machineAppImpl) ToMachineInfoById(machineId uint64) (*mcm.MachineInfo, error) {
me, authCert, err := m.getMachineAndAuthCert(machineId)
if err != nil {
return nil, err
}
return m.toMi(me, authCert)
}
func (m *machineAppImpl) getMachineAndAuthCert(machineId uint64) (*entity.Machine, *tagentity.ResourceAuthCert, error) {
me, err := m.GetById(new(entity.Machine), machineId)
if err != nil {
return nil, nil, errorx.NewBiz("[%d]机器信息不存在", machineId)
}
if me.Status != entity.MachineStatusEnable && me.Protocol == 1 {
return nil, nil, errorx.NewBiz("[%s]该机器已被停用", me.Code)
}
authCert, err := m.resourceAuthCertApp.GetResourceAuthCert(tagentity.TagTypeMachine, me.Code)
if err != nil {
return nil, nil, err
}
return me, authCert, nil
}
func (m *machineAppImpl) toMi(me *entity.Machine, authCert *tagentity.ResourceAuthCert) (*mcm.MachineInfo, error) {
mi := new(mcm.MachineInfo)
mi.Id = me.Id
mi.Name = me.Name
mi.Ip = me.Ip
mi.Port = me.Port
mi.Username = me.Username
mi.TagPath = m.tagApp.ListTagPathByResource(consts.TagResourceTypeMachine, me.Code)
mi.TagPath = m.tagApp.ListTagPathByResource(int8(tagentity.TagTypeMachineAuthCert), authCert.Name)
mi.EnableRecorder = me.EnableRecorder
mi.Protocol = me.Protocol
if me.UseAuthCert() {
ac, err := m.authCertApp.GetById(new(entity.AuthCert), uint64(me.AuthCertId))
if err != nil {
return nil, errorx.NewBiz("授权凭证信息已不存在,请重新关联")
}
mi.AuthMethod = ac.AuthMethod
if err := ac.PwdDecrypt(); err != nil {
return nil, errorx.NewBiz(err.Error())
}
mi.Password = ac.Password
mi.Passphrase = ac.Passphrase
} else {
mi.AuthMethod = entity.AuthCertAuthMethodPassword
if me.Id != 0 {
if err := me.PwdDecrypt(); err != nil {
return nil, errorx.NewBiz(err.Error())
}
}
mi.Password = me.Password
}
mi.Username = authCert.Username
mi.Password = authCert.Ciphertext
mi.Passphrase = cast.ToString(authCert.Extra["passphrase"])
mi.AuthMethod = int8(authCert.CiphertextType)
// 使用了ssh隧道则将隧道机器信息也附上
if me.SshTunnelMachineId > 0 {
sshTunnelMe, err := m.GetById(new(entity.Machine), uint64(me.SshTunnelMachineId))
if err != nil {
return nil, errorx.NewBiz("隧道机器信息不存在")
}
sshTunnelMi, err := m.toMachineInfo(sshTunnelMe)
sshTunnelMi, err := m.ToMachineInfoById(uint64(me.SshTunnelMachineId))
if err != nil {
return nil, err
}

View File

@@ -6,8 +6,6 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"mayfly-go/internal/machine/api/form"
"mayfly-go/internal/machine/config"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
@@ -17,6 +15,7 @@ import (
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/bytex"
"mayfly-go/pkg/utils/collx"
"mime/multipart"
"os"
"path/filepath"
@@ -25,6 +24,13 @@ import (
"github.com/pkg/sftp"
)
type MachineFileOpParam struct {
MachineId uint64 `json:"machineId" binding:"required" form:"machineId"`
Protocol int `json:"protocol" binding:"required" form:"protocol"`
AuthCertName string `json:"authCertName" binding:"required" form:"authCertName"` // 授权凭证
Path string `json:"path" form:"path"` // 文件路径
}
type MachineFile interface {
base.App[*entity.MachineFile]
@@ -36,50 +42,47 @@ type MachineFile interface {
Save(ctx context.Context, entity *entity.MachineFile) error
// 获取文件关联的机器信息,主要用于记录日志使用
// GetMachine(fileId uint64) *mcm.Info
// 检查文件路径并返回机器id
GetMachineCli(fileId uint64, path ...string) (*mcm.Cli, error)
// 获取机器cli
GetMachineCli(authCertName string) (*mcm.Cli, error)
GetRdpFilePath(MachineId uint64, path string) string
/** sftp 相关操作 **/
// 创建目录
MkDir(fid uint64, path string, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error)
MkDir(opParam *MachineFileOpParam) (*mcm.MachineInfo, error)
// 创建文件
CreateFile(fid uint64, path string, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error)
CreateFile(opParam *MachineFileOpParam) (*mcm.MachineInfo, error)
// 读取目录
ReadDir(fid uint64, opForm *form.ServerFileOptionForm) ([]fs.FileInfo, error)
ReadDir(opParam *MachineFileOpParam) ([]fs.FileInfo, error)
// 获取指定目录内容大小
GetDirSize(fid uint64, opForm *form.ServerFileOptionForm) (string, error)
GetDirSize(opParam *MachineFileOpParam) (string, error)
// 获取文件stat
FileStat(opForm *form.ServerFileOptionForm) (string, error)
FileStat(opParam *MachineFileOpParam) (string, error)
// 读取文件内容
ReadFile(fileId uint64, path string) (*sftp.File, *mcm.MachineInfo, error)
ReadFile(opParam *MachineFileOpParam) (*sftp.File, *mcm.MachineInfo, error)
// 写文件
WriteFileContent(fileId uint64, path string, content []byte, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error)
WriteFileContent(opParam *MachineFileOpParam, content []byte) (*mcm.MachineInfo, error)
// 文件上传
UploadFile(fileId uint64, path, filename string, reader io.Reader, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error)
UploadFile(opParam *MachineFileOpParam, filename string, reader io.Reader) (*mcm.MachineInfo, error)
UploadFiles(basePath string, fileHeaders []*multipart.FileHeader, paths []string, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error)
UploadFiles(opParam *MachineFileOpParam, basePath string, fileHeaders []*multipart.FileHeader, paths []string) (*mcm.MachineInfo, error)
// 移除文件
RemoveFile(opForm *form.MachineFileOpForm) (*mcm.MachineInfo, error)
RemoveFile(opParam *MachineFileOpParam, path ...string) (*mcm.MachineInfo, error)
Copy(opForm *form.MachineFileOpForm) (*mcm.MachineInfo, error)
Copy(opParam *MachineFileOpParam, toPath string, path ...string) (*mcm.MachineInfo, error)
Mv(opForm *form.MachineFileOpForm) (*mcm.MachineInfo, error)
Mv(opParam *MachineFileOpParam, toPath string, path ...string) (*mcm.MachineInfo, error)
Rename(renameForm *form.MachineFileRename) (*mcm.MachineInfo, error)
Rename(opParam *MachineFileOpParam, newname string) (*mcm.MachineInfo, error)
}
type machineFileAppImpl struct {
@@ -117,29 +120,37 @@ func (m *machineFileAppImpl) Save(ctx context.Context, mf *entity.MachineFile) e
return m.Insert(ctx, mf)
}
func (m *machineFileAppImpl) ReadDir(fid uint64, opForm *form.ServerFileOptionForm) ([]fs.FileInfo, error) {
if !strings.HasSuffix(opForm.Path, "/") {
opForm.Path = opForm.Path + "/"
func (m *machineFileAppImpl) ReadDir(opParam *MachineFileOpParam) ([]fs.FileInfo, error) {
path := opParam.Path
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
// 如果是rdp则直接读取本地文件
if opForm.Protocol == entity.MachineProtocolRdp {
opForm.Path = m.GetRdpFilePath(opForm.MachineId, opForm.Path)
return ioutil.ReadDir(opForm.Path)
if opParam.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opParam.MachineId, path)
dirs, err := os.ReadDir(path)
if err != nil {
return nil, err
}
return collx.ArrayMap[fs.DirEntry, fs.FileInfo](dirs, func(val fs.DirEntry) fs.FileInfo {
fi, _ := val.Info()
return fi
}), nil
}
_, sftpCli, err := m.GetMachineSftpCli(fid, opForm.Path)
_, sftpCli, err := m.GetMachineSftpCli(opParam)
if err != nil {
return nil, err
}
return sftpCli.ReadDir(opForm.Path)
return sftpCli.ReadDir(path)
}
func (m *machineFileAppImpl) GetDirSize(fid uint64, opForm *form.ServerFileOptionForm) (string, error) {
path := opForm.Path
func (m *machineFileAppImpl) GetDirSize(opParam *MachineFileOpParam) (string, error) {
path := opParam.Path
if opForm.Protocol == entity.MachineProtocolRdp {
dirPath := m.GetRdpFilePath(opForm.MachineId, path)
if opParam.Protocol == entity.MachineProtocolRdp {
dirPath := m.GetRdpFilePath(opParam.MachineId, path)
// 递归计算目录下文件大小
var totalSize int64
@@ -160,7 +171,7 @@ func (m *machineFileAppImpl) GetDirSize(fid uint64, opForm *form.ServerFileOptio
return bytex.FormatSize(totalSize), nil
}
mcli, err := m.GetMachineCli(fid, path)
mcli, err := m.GetMachineCli(opParam.AuthCertName)
if err != nil {
return "", err
}
@@ -184,32 +195,34 @@ func (m *machineFileAppImpl) GetDirSize(fid uint64, opForm *form.ServerFileOptio
return strings.Split(res, "\t")[0], nil
}
func (m *machineFileAppImpl) FileStat(opForm *form.ServerFileOptionForm) (string, error) {
if opForm.Protocol == entity.MachineProtocolRdp {
path := m.GetRdpFilePath(opForm.MachineId, opForm.Path)
func (m *machineFileAppImpl) FileStat(opParam *MachineFileOpParam) (string, error) {
path := opParam.Path
if opParam.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opParam.MachineId, path)
stat, err := os.Stat(path)
return fmt.Sprintf("%v", stat), err
}
mcli, err := m.GetMachineCli(opForm.FileId, opForm.Path)
mcli, err := m.GetMachineCli(opParam.AuthCertName)
if err != nil {
return "", err
}
return mcli.Run(fmt.Sprintf("stat -L %s", opForm.Path))
return mcli.Run(fmt.Sprintf("stat -L %s", path))
}
func (m *machineFileAppImpl) MkDir(fid uint64, path string, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error) {
func (m *machineFileAppImpl) MkDir(opParam *MachineFileOpParam) (*mcm.MachineInfo, error) {
path := opParam.Path
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
if opForm.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opForm.MachineId, path)
if opParam.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opParam.MachineId, path)
os.MkdirAll(path, os.ModePerm)
return nil, nil
}
mi, sftpCli, err := m.GetMachineSftpCli(fid, path)
mi, sftpCli, err := m.GetMachineSftpCli(opParam)
if err != nil {
return nil, err
}
@@ -218,14 +231,15 @@ func (m *machineFileAppImpl) MkDir(fid uint64, path string, opForm *form.ServerF
return mi, err
}
func (m *machineFileAppImpl) CreateFile(fid uint64, path string, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error) {
mi, sftpCli, err := m.GetMachineSftpCli(fid, path)
func (m *machineFileAppImpl) CreateFile(opParam *MachineFileOpParam) (*mcm.MachineInfo, error) {
mi, sftpCli, err := m.GetMachineSftpCli(opParam)
if err != nil {
return nil, err
}
if opForm.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opForm.MachineId, path)
path := opParam.Path
if opParam.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opParam.MachineId, path)
file, err := os.Create(path)
defer file.Close()
return nil, err
@@ -239,22 +253,22 @@ func (m *machineFileAppImpl) CreateFile(fid uint64, path string, opForm *form.Se
return mi, err
}
func (m *machineFileAppImpl) ReadFile(fileId uint64, path string) (*sftp.File, *mcm.MachineInfo, error) {
mi, sftpCli, err := m.GetMachineSftpCli(fileId, path)
func (m *machineFileAppImpl) ReadFile(opParam *MachineFileOpParam) (*sftp.File, *mcm.MachineInfo, error) {
mi, sftpCli, err := m.GetMachineSftpCli(opParam)
if err != nil {
return nil, nil, err
}
// 读取文件内容
fc, err := sftpCli.Open(path)
fc, err := sftpCli.Open(opParam.Path)
return fc, mi, err
}
// 写文件内容
func (m *machineFileAppImpl) WriteFileContent(fileId uint64, path string, content []byte, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error) {
if opForm.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opForm.MachineId, path)
func (m *machineFileAppImpl) WriteFileContent(opParam *MachineFileOpParam, content []byte) (*mcm.MachineInfo, error) {
path := opParam.Path
if opParam.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opParam.MachineId, path)
file, err := os.Create(path)
defer file.Close()
if err != nil {
@@ -264,7 +278,7 @@ func (m *machineFileAppImpl) WriteFileContent(fileId uint64, path string, conten
return nil, err
}
mi, sftpCli, err := m.GetMachineSftpCli(fileId, path)
mi, sftpCli, err := m.GetMachineSftpCli(opParam)
if err != nil {
return nil, err
}
@@ -279,13 +293,14 @@ func (m *machineFileAppImpl) WriteFileContent(fileId uint64, path string, conten
}
// 上传文件
func (m *machineFileAppImpl) UploadFile(fileId uint64, path, filename string, reader io.Reader, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error) {
func (m *machineFileAppImpl) UploadFile(opParam *MachineFileOpParam, filename string, reader io.Reader) (*mcm.MachineInfo, error) {
path := opParam.Path
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
if opForm.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opForm.MachineId, path)
if opParam.Protocol == entity.MachineProtocolRdp {
path = m.GetRdpFilePath(opParam.MachineId, path)
file, err := os.Create(path + filename)
defer file.Close()
if err != nil {
@@ -295,7 +310,7 @@ func (m *machineFileAppImpl) UploadFile(fileId uint64, path, filename string, re
return nil, nil
}
mi, sftpCli, err := m.GetMachineSftpCli(fileId, path)
mi, sftpCli, err := m.GetMachineSftpCli(opParam)
if err != nil {
return nil, err
}
@@ -309,9 +324,9 @@ func (m *machineFileAppImpl) UploadFile(fileId uint64, path, filename string, re
return mi, err
}
func (m *machineFileAppImpl) UploadFiles(basePath string, fileHeaders []*multipart.FileHeader, paths []string, opForm *form.ServerFileOptionForm) (*mcm.MachineInfo, error) {
if opForm.Protocol == entity.MachineProtocolRdp {
baseFolder := m.GetRdpFilePath(opForm.MachineId, basePath)
func (m *machineFileAppImpl) UploadFiles(opParam *MachineFileOpParam, basePath string, fileHeaders []*multipart.FileHeader, paths []string) (*mcm.MachineInfo, error) {
if opParam.Protocol == entity.MachineProtocolRdp {
baseFolder := m.GetRdpFilePath(opParam.MachineId, basePath)
for i, fileHeader := range fileHeaders {
file, err := fileHeader.Open()
@@ -326,7 +341,11 @@ func (m *machineFileAppImpl) UploadFiles(basePath string, fileHeaders []*multipa
rdpBaseDir = rdpBaseDir + "/"
}
rdpDir := filepath.Dir(rdpBaseDir + paths[i])
m.MkDir(0, rdpDir, opForm)
m.MkDir(&MachineFileOpParam{
MachineId: opParam.MachineId,
Protocol: opParam.Protocol,
Path: rdpDir,
})
// 创建文件
if !strings.HasSuffix(baseFolder, "/") {
@@ -348,24 +367,23 @@ func (m *machineFileAppImpl) UploadFiles(basePath string, fileHeaders []*multipa
}
// 删除文件
func (m *machineFileAppImpl) RemoveFile(opForm *form.MachineFileOpForm) (*mcm.MachineInfo, error) {
if opForm.Protocol == entity.MachineProtocolRdp {
for _, pt := range opForm.Path {
pt = m.GetRdpFilePath(opForm.MachineId, pt)
func (m *machineFileAppImpl) RemoveFile(opParam *MachineFileOpParam, path ...string) (*mcm.MachineInfo, error) {
if opParam.Protocol == entity.MachineProtocolRdp {
for _, pt := range path {
pt = m.GetRdpFilePath(opParam.MachineId, pt)
os.RemoveAll(pt)
}
return nil, nil
}
mcli, err := m.GetMachineCli(opForm.FileId, opForm.Path...)
mcli, err := m.GetMachineCli(opParam.AuthCertName)
if err != nil {
return nil, err
}
minfo := mcli.Info
// 优先使用命令删除速度快sftp需要递归遍历删除子文件等
res, err := mcli.Run(fmt.Sprintf("rm -rf %s", strings.Join(opForm.Path, " ")))
res, err := mcli.Run(fmt.Sprintf("rm -rf %s", strings.Join(path, " ")))
if err == nil {
return minfo, nil
}
@@ -376,7 +394,7 @@ func (m *machineFileAppImpl) RemoveFile(opForm *form.MachineFileOpForm) (*mcm.Ma
return minfo, err
}
for _, p := range opForm.Path {
for _, p := range path {
err = sftpCli.RemoveAll(p)
if err != nil {
break
@@ -385,11 +403,11 @@ func (m *machineFileAppImpl) RemoveFile(opForm *form.MachineFileOpForm) (*mcm.Ma
return minfo, err
}
func (m *machineFileAppImpl) Copy(opForm *form.MachineFileOpForm) (*mcm.MachineInfo, error) {
if opForm.Protocol == entity.MachineProtocolRdp {
for _, pt := range opForm.Path {
srcPath := m.GetRdpFilePath(opForm.MachineId, pt)
targetPath := m.GetRdpFilePath(opForm.MachineId, opForm.ToPath+pt)
func (m *machineFileAppImpl) Copy(opParam *MachineFileOpParam, toPath string, path ...string) (*mcm.MachineInfo, error) {
if opParam.Protocol == entity.MachineProtocolRdp {
for _, pt := range path {
srcPath := m.GetRdpFilePath(opParam.MachineId, pt)
targetPath := m.GetRdpFilePath(opParam.MachineId, toPath+pt)
// 打开源文件
srcFile, err := os.Open(srcPath)
@@ -408,59 +426,57 @@ func (m *machineFileAppImpl) Copy(opForm *form.MachineFileOpForm) (*mcm.MachineI
return nil, nil
}
mcli, err := m.GetMachineCli(opForm.FileId, opForm.Path...)
mcli, err := m.GetMachineCli(opParam.AuthCertName)
if err != nil {
return nil, err
}
mi := mcli.Info
res, err := mcli.Run(fmt.Sprintf("cp -r %s %s", strings.Join(opForm.Path, " "), opForm.ToPath))
res, err := mcli.Run(fmt.Sprintf("cp -r %s %s", strings.Join(path, " "), toPath))
if err != nil {
return mi, errors.New(res)
}
return mi, err
}
func (m *machineFileAppImpl) Mv(opForm *form.MachineFileOpForm) (*mcm.MachineInfo, error) {
if opForm.Protocol == entity.MachineProtocolRdp {
for _, pt := range opForm.Path {
func (m *machineFileAppImpl) Mv(opParam *MachineFileOpParam, toPath string, path ...string) (*mcm.MachineInfo, error) {
if opParam.Protocol == entity.MachineProtocolRdp {
for _, pt := range path {
// 获取文件名
filename := filepath.Base(pt)
topath := opForm.ToPath
if !strings.HasSuffix(topath, "/") {
topath += "/"
if !strings.HasSuffix(toPath, "/") {
toPath += "/"
}
srcPath := m.GetRdpFilePath(opForm.MachineId, pt)
targetPath := m.GetRdpFilePath(opForm.MachineId, topath+filename)
srcPath := m.GetRdpFilePath(opParam.MachineId, pt)
targetPath := m.GetRdpFilePath(opParam.MachineId, toPath+filename)
os.Rename(srcPath, targetPath)
}
return nil, nil
}
mcli, err := m.GetMachineCli(opForm.FileId, opForm.Path...)
mcli, err := m.GetMachineCli(opParam.AuthCertName)
if err != nil {
return nil, err
}
mi := mcli.Info
res, err := mcli.Run(fmt.Sprintf("mv %s %s", strings.Join(opForm.Path, " "), opForm.ToPath))
res, err := mcli.Run(fmt.Sprintf("mv %s %s", strings.Join(path, " "), toPath))
if err != nil {
return mi, errorx.NewBiz(res)
}
return mi, err
}
func (m *machineFileAppImpl) Rename(renameForm *form.MachineFileRename) (*mcm.MachineInfo, error) {
oldname := renameForm.Oldname
newname := renameForm.Newname
if renameForm.Protocol == entity.MachineProtocolRdp {
oldname = m.GetRdpFilePath(renameForm.MachineId, renameForm.Oldname)
newname = m.GetRdpFilePath(renameForm.MachineId, renameForm.Newname)
func (m *machineFileAppImpl) Rename(opParam *MachineFileOpParam, newname string) (*mcm.MachineInfo, error) {
oldname := opParam.Path
if opParam.Protocol == entity.MachineProtocolRdp {
oldname = m.GetRdpFilePath(opParam.MachineId, oldname)
newname = m.GetRdpFilePath(opParam.MachineId, newname)
return nil, os.Rename(oldname, newname)
}
mi, sftpCli, err := m.GetMachineSftpCli(renameForm.FileId, newname)
mi, sftpCli, err := m.GetMachineSftpCli(opParam)
if err != nil {
return nil, err
}
@@ -468,24 +484,13 @@ func (m *machineFileAppImpl) Rename(renameForm *form.MachineFileRename) (*mcm.Ma
}
// 获取文件机器cli
func (m *machineFileAppImpl) GetMachineCli(fid uint64, inputPath ...string) (*mcm.Cli, error) {
mf, err := m.GetById(new(entity.MachineFile), fid)
if err != nil {
return nil, errorx.NewBiz("文件不存在")
}
for _, path := range inputPath {
// 接口传入的地址需为配置路径的子路径
if !strings.HasPrefix(path, mf.Path) {
return nil, errorx.NewBiz("无权访问该目录或文件: %s", path)
}
}
return m.machineApp.GetCli(mf.MachineId)
func (m *machineFileAppImpl) GetMachineCli(authCertName string) (*mcm.Cli, error) {
return m.machineApp.GetCliByAc(authCertName)
}
// 获取文件机器 sftp cli
func (m *machineFileAppImpl) GetMachineSftpCli(fid uint64, inputPath ...string) (*mcm.MachineInfo, *sftp.Client, error) {
mcli, err := m.GetMachineCli(fid, inputPath...)
func (m *machineFileAppImpl) GetMachineSftpCli(opParam *MachineFileOpParam) (*mcm.MachineInfo, *sftp.Client, error) {
mcli, err := m.GetMachineCli(opParam.AuthCertName)
if err != nil {
return nil, nil, err
}
@@ -498,6 +503,6 @@ func (m *machineFileAppImpl) GetMachineSftpCli(fid uint64, inputPath ...string)
return mcli.Info, sftpCli, nil
}
func (m *machineFileAppImpl) GetRdpFilePath(MachineId uint64, path string) string {
return fmt.Sprintf("%s/%d%s", config.GetMachine().GuacdFilePath, MachineId, path)
func (m *machineFileAppImpl) GetRdpFilePath(machineId uint64, path string) string {
return fmt.Sprintf("%s/%d%s", config.GetMachine().GuacdFilePath, machineId, path)
}

View File

@@ -1,8 +1,6 @@
package entity
import (
"errors"
"mayfly-go/internal/common/utils"
"mayfly-go/pkg/model"
)
@@ -14,9 +12,6 @@ type Machine struct {
Protocol int `json:"protocol"` // 连接协议 1.ssh 2.rdp
Ip string `json:"ip"` // IP地址
Port int `json:"port"` // 端口号
Username string `json:"username"` // 用户名
Password string `json:"password"` // 密码
AuthCertId int `json:"authCertId"` // 授权凭证id
Status int8 `json:"status"` // 状态 1:启用2:停用
Remark string `json:"remark"` // 备注
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
@@ -30,27 +25,3 @@ const (
MachineProtocolSsh = 1
MachineProtocolRdp = 2
)
func (m *Machine) PwdEncrypt() error {
// 密码替换为加密后的密码
password, err := utils.PwdAesEncrypt(m.Password)
if err != nil {
return errors.New("加密主机密码失败")
}
m.Password = password
return nil
}
func (m *Machine) PwdDecrypt() error {
// 密码替换为解密后的密码
password, err := utils.PwdAesDecrypt(m.Password)
if err != nil {
return errors.New("解密主机密码失败")
}
m.Password = password
return nil
}
func (m *Machine) UseAuthCert() bool {
return m.AuthCertId > 0
}

View File

@@ -16,7 +16,7 @@ import (
)
// creates the tunnel to the remote machine (via guacd)
func DoConnect(query url.Values, parameters map[string]string, machineId uint64) (Tunnel, error) {
func DoConnect(query url.Values, parameters map[string]string, ac string) (Tunnel, error) {
conf := NewGuacamoleConfiguration()
parameters["enable-wallpaper"] = "true" // 允许显示墙纸
@@ -33,7 +33,7 @@ func DoConnect(query url.Values, parameters map[string]string, machineId uint64)
parameters["enable-drive"] = "true"
parameters["drive-name"] = "Filesystem"
parameters["create-drive-path"] = "true"
parameters["drive-path"] = fmt.Sprintf("/rdp-file/%d", machineId)
parameters["drive-path"] = fmt.Sprintf("/rdp-file/%s", ac)
conf.Protocol = parameters["scheme"]
conf.Parameters = parameters

View File

@@ -1,34 +1,35 @@
package guac
import (
"github.com/gorilla/websocket"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
// MemorySessionStore is a simple in-memory store of connected sessions that is used by
// the WebsocketServer to store active sessions.
type MemorySessionStore struct {
sync.RWMutex
ConnIds map[uint64]Tunnel
ConnIds map[string]Tunnel
}
// NewMemorySessionStore creates a new store
func NewMemorySessionStore() *MemorySessionStore {
return &MemorySessionStore{
ConnIds: map[uint64]Tunnel{},
ConnIds: map[string]Tunnel{},
}
}
// Get returns a connection by uuid
func (s *MemorySessionStore) Get(id uint64) Tunnel {
func (s *MemorySessionStore) Get(id string) Tunnel {
s.RLock()
defer s.RUnlock()
return s.ConnIds[id]
}
// Add inserts a new connection by uuid
func (s *MemorySessionStore) Add(id uint64, conn *websocket.Conn, req *http.Request, tunnel Tunnel) {
func (s *MemorySessionStore) Add(id string, conn *websocket.Conn, req *http.Request, tunnel Tunnel) {
s.Lock()
defer s.Unlock()
n, ok := s.ConnIds[id]
@@ -41,7 +42,7 @@ func (s *MemorySessionStore) Add(id uint64, conn *websocket.Conn, req *http.Requ
}
// Delete removes a connection by uuid
func (s *MemorySessionStore) Delete(id uint64, conn *websocket.Conn, req *http.Request, tunnel Tunnel) {
func (s *MemorySessionStore) Delete(id string, conn *websocket.Conn, req *http.Request, tunnel Tunnel) {
s.Lock()
defer s.Unlock()
n, ok := s.ConnIds[id]

View File

@@ -1,7 +1,9 @@
package mcm
import (
"errors"
"mayfly-go/internal/common/consts"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/logx"
"time"
@@ -29,29 +31,63 @@ func init() {
go checkClientAvailability(3 * time.Minute)
}
// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建
func GetMachineCli(machineId uint64, getMachine func(uint64) (*MachineInfo, error)) (*Cli, error) {
if load, ok := cliCache.Get(machineId); ok {
// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建
// @param 机器的授权凭证名
func GetMachineCli(authCertName string, getMachine func(string) (*MachineInfo, error)) (*Cli, error) {
if load, ok := cliCache.Get(authCertName); ok {
return load.(*Cli), nil
}
me, err := getMachine(machineId)
mi, err := getMachine(authCertName)
if err != nil {
return nil, err
}
mi.Key = authCertName
c, err := mi.Conn()
if err != nil {
return nil, err
}
c, err := me.Conn()
if err != nil {
return nil, err
}
cliCache.Put(machineId, c)
cliCache.Put(authCertName, c)
return c, nil
}
// 根据机器id从已连接的机器客户端中获取特权账号连接, 若不存在特权账号,则随机返回一个
func GetMachineCliById(machineId uint64) (*Cli, error) {
// 遍历所有机器连接实例删除指定机器id关联的连接...
items := cliCache.Items()
var machineCli *Cli
for _, v := range items {
cli := v.Value.(*Cli)
mi := cli.Info
if mi.Id != machineId {
continue
}
machineCli = cli
// 如果是特权账号,则跳出
if mi.AuthCertType == tagentity.AuthCertTypePrivileged {
break
}
}
if machineCli != nil {
return machineCli, nil
}
return nil, errors.New("不存在该机器id的连接")
}
// 删除指定机器缓存客户端,并关闭客户端连接
func DeleteCli(id uint64) {
cliCache.Delete(id)
// 遍历所有机器连接实例删除指定机器id关联的连接...
items := cliCache.Items()
for _, v := range items {
mi := v.Value.(*Cli).Info
if mi.Id == id {
cliCache.Delete(mi.Key)
}
}
}
// 检查缓存中的客户端是否可用,不可用则关闭客户端连接

View File

@@ -2,7 +2,7 @@ package mcm
import (
"fmt"
"mayfly-go/internal/machine/domain/entity"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"net"
@@ -13,16 +13,20 @@ import (
// 机器信息
type MachineInfo struct {
Key string `json:"key"` // 缓存key
Id uint64 `json:"id"`
Name string `json:"name"`
Protocol int `json:"protocol"`
Ip string `json:"ip"` // IP地址
Port int `json:"-"` // 端口号
AuthMethod int8 `json:"-"` // 授权认证方式
Username string `json:"-"` // 用户名
Password string `json:"-"`
Passphrase string `json:"-"` // 私钥口令
Ip string `json:"ip"` // IP地址
Port int `json:"-"` // 端口号
AuthCertName string `json:"authCertName"`
AuthCertType tagentity.AuthCertType `json:"-"`
AuthMethod int8 `json:"-"` // 授权认证方式
Username string `json:"-"` // 用户名
Password string `json:"-"`
Passphrase string `json:"-"` // 私钥口令
SshTunnelMachine *MachineInfo `json:"-"` // ssh隧道机器
TempSshMachineId uint64 `json:"-"` // ssh隧道机器id用于记录隧道机器id连接出错后关闭隧道
@@ -118,9 +122,9 @@ func GetSshClient(m *MachineInfo, jumpClient *ssh.Client) (*ssh.Client, error) {
},
Timeout: 5 * time.Second,
}
if m.AuthMethod == entity.AuthCertAuthMethodPassword {
if m.AuthMethod == int8(tagentity.AuthCertCiphertextTypePassword) {
config.Auth = []ssh.AuthMethod{ssh.Password(m.Password)}
} else if m.AuthMethod == entity.MachineAuthMethodPublicKey {
} else if m.AuthMethod == int8(tagentity.AuthCertCiphertextTypePrivateKey) {
var key ssh.Signer
var err error

View File

@@ -49,9 +49,9 @@ func InitMachineRouter(router *gin.RouterGroup) {
req.BatchSetGroup(machines, reqs[:])
// 终端连接
machines.GET(":machineId/terminal", m.WsSSH)
machines.GET("terminal/:ac", m.WsSSH)
// 终端连接
machines.GET(":machineId/rdp", m.WsGuacamole)
machines.GET("rdp/:ac", m.WsGuacamole)
}
}

View File

@@ -7,6 +7,7 @@ import (
"mayfly-go/internal/mongo/domain/repository"
"mayfly-go/internal/mongo/mgm"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
@@ -59,7 +60,7 @@ func (d *mongoAppImpl) Delete(ctx context.Context, id uint64) error {
},
func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: consts.TagResourceTypeMongo,
ResourceType: tagentity.TagTypeMongo,
ResourceCode: mongoEntity.Code,
})
})
@@ -90,7 +91,7 @@ func (d *mongoAppImpl) SaveMongo(ctx context.Context, m *entity.Mongo, tagIds ..
return d.Insert(ctx, m)
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: consts.TagResourceTypeMongo,
ResourceType: tagentity.TagTypeMongo,
ResourceCode: m.Code,
TagIds: tagIds,
})
@@ -113,7 +114,7 @@ func (d *mongoAppImpl) SaveMongo(ctx context.Context, m *entity.Mongo, tagIds ..
return d.UpdateById(ctx, m)
}, func(ctx context.Context) error {
return d.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: consts.TagResourceTypeMongo,
ResourceType: tagentity.TagTypeMongo,
ResourceCode: oldMongo.Code,
TagIds: tagIds,
})

View File

@@ -9,6 +9,7 @@ import (
"mayfly-go/internal/redis/domain/repository"
"mayfly-go/internal/redis/rdm"
tagapp "mayfly-go/internal/tag/application"
tagenttiy "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
@@ -107,7 +108,7 @@ func (r *redisAppImpl) SaveRedis(ctx context.Context, re *entity.Redis, tagIds .
return r.Insert(ctx, re)
}, func(ctx context.Context) error {
return r.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: consts.TagResourceTypeRedis,
ResourceType: tagenttiy.TagTypeRedis,
ResourceCode: re.Code,
TagIds: tagIds,
})
@@ -138,7 +139,7 @@ func (r *redisAppImpl) SaveRedis(ctx context.Context, re *entity.Redis, tagIds .
return r.UpdateById(ctx, re)
}, func(ctx context.Context) error {
return r.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: consts.TagResourceTypeRedis,
ResourceType: tagenttiy.TagTypeRedis,
ResourceCode: oldRedis.Code,
TagIds: tagIds,
})
@@ -161,7 +162,7 @@ func (r *redisAppImpl) Delete(ctx context.Context, id uint64) error {
return r.DeleteById(ctx, id)
}, func(ctx context.Context) error {
return r.tagApp.SaveResource(ctx, &tagapp.SaveResourceTagParam{
ResourceType: consts.TagResourceTypeRedis,
ResourceType: tagenttiy.TagTypeRedis,
ResourceCode: re.Code,
})
})

View File

@@ -0,0 +1,29 @@
package api
import (
"mayfly-go/internal/tag/application"
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
)
type ResourceAuthCert struct {
ResourceAuthCertApp application.ResourceAuthCert `inject:""`
}
func (r *ResourceAuthCert) ListByQuery(rc *req.Ctx) {
cond := new(entity.ResourceAuthCert)
cond.ResourceCode = rc.Query("resourceCode")
cond.ResourceType = int8(rc.QueryInt("resourceType"))
cond.Type = entity.AuthCertType(rc.QueryInt("type"))
cond.CiphertextType = entity.AuthCertCiphertextType(rc.QueryInt("ciphertextType"))
cond.Name = rc.Query("name")
var racs []*entity.ResourceAuthCert
res, err := r.ResourceAuthCertApp.PageQuery(cond, rc.GetPageParam(), &racs)
biz.ErrIsNil(err)
for _, rac := range racs {
rac.CiphertextDecrypt()
}
rc.ResData = res
}

View File

@@ -15,14 +15,16 @@ import (
type TagTree struct {
TagTreeApp application.TagTree `inject:""`
ResourceAuthCertApp application.ResourceAuthCert `inject:""`
}
func (p *TagTree) GetTagTree(rc *req.Ctx) {
tagType := rc.QueryInt("type")
tagType := entity.TagType(rc.QueryInt("type"))
// 超管返回所有标签树
if rc.GetLoginAccount().Id == consts.AdminId {
var tagTrees vo.TagTreeVOS
p.TagTreeApp.ListByQuery(&entity.TagTreeQuery{Type: int8(tagType)}, &tagTrees)
p.TagTreeApp.ListByQuery(&entity.TagTreeQuery{Type: tagType}, &tagTrees)
rc.ResData = tagTrees.ToTrees(0)
return
}
@@ -40,7 +42,7 @@ func (p *TagTree) GetTagTree(rc *req.Ctx) {
// 获取所有以root标签开头的子标签
var tags []*entity.TagTree
p.TagTreeApp.ListByQuery(&entity.TagTreeQuery{CodePathLikes: collx.MapKeys(rootTag), Type: int8(tagType)}, &tags)
p.TagTreeApp.ListByQuery(&entity.TagTreeQuery{CodePathLikes: collx.MapKeys(rootTag), Type: tagType}, &tags)
tagTrees := make(vo.TagTreeVOS, 0)
for _, tag := range tags {
@@ -81,12 +83,14 @@ func (p *TagTree) DelTagTree(rc *req.Ctx) {
biz.ErrIsNil(p.TagTreeApp.Delete(rc.MetaCtx, uint64(rc.PathParamInt("id"))))
}
// 获取用户可操作的资源标签路径
// 获取用户可操作的标签路径
func (p *TagTree) TagResources(rc *req.Ctx) {
resourceType := int8(rc.PathParamInt("rtype"))
tagResources := p.TagTreeApp.GetAccountTagResources(rc.GetLoginAccount().Id, resourceType, "")
tagPath2Resource := collx.ArrayToMap[entity.TagTree, string](tagResources, func(tagResource entity.TagTree) string {
return tagResource.GetParentPath()
accountId := rc.GetLoginAccount().Id
tagResources := p.TagTreeApp.GetAccountTagResources(accountId, &entity.TagTreeQuery{Type: entity.TagType(resourceType)})
tagPath2Resource := collx.ArrayToMap[*entity.TagTree, string](tagResources, func(tagResource *entity.TagTree) string {
return tagResource.GetParentPath(1)
})
tagPaths := collx.MapKeys(tagPath2Resource)

View File

@@ -0,0 +1,22 @@
package vo
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/model"
"time"
)
type ResourceAuthCert struct {
Id uint64 `json:"id"`
Name string `json:"name"` // 名称
ResourceCode string `json:"resourceCode"` // 资源编号
ResourceType int8 `json:"resourceType"` // 资源类型
Username string `json:"username"` // 用户名
Ciphertext string `json:"ciphertext"` // 密文
CiphertextType entity.AuthCertCiphertextType `json:"ciphertextType"` // 密文类型
Extra model.Map[string, any] `json:"extra"` // 账号需要的其他额外信息(如秘钥口令等)
Type entity.AuthCertType `json:"type"` // 凭证类型
Remark string `json:"remark"` // 备注
CreateTime *time.Time `json:"createTime"`
}

View File

@@ -7,4 +7,5 @@ import (
func InitIoc() {
ioc.Register(new(tagTreeAppImpl), ioc.WithComponentName("TagTreeApp"))
ioc.Register(new(teamAppImpl), ioc.WithComponentName("TeamApp"))
ioc.Register(new(resourceAuthCertAppImpl), ioc.WithComponentName("ResourceAuthCertApp"))
}

View File

@@ -0,0 +1,268 @@
package application
import (
"context"
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/utils/collx"
)
type SaveAuthCertParam struct {
ResourceCode string
// 资源标签类型
ResourceType entity.TagType
// 授权凭证类型
AuthCertTagType entity.TagType
// 空数组则为删除该资源绑定的授权凭证
AuthCerts []*entity.ResourceAuthCert
}
type ResourceAuthCert interface {
base.App[*entity.ResourceAuthCert]
// SaveAuthCert 保存资源授权凭证信息,不可放于事务中
SaveAuthCert(ctx context.Context, param *SaveAuthCertParam) error
// GetAuthCert 根据授权凭证名称获取授权凭证
GetAuthCert(authCertName string) (*entity.ResourceAuthCert, error)
// GetResourceAuthCert 获取资源授权凭证,默认获取特权账号,若没有则返回第一个
GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error)
// GetAccountAuthCert 获取账号有权限操作的授权凭证信息
GetAccountAuthCert(accountId uint64, authCertTagType entity.TagType, tagPath ...string) []*entity.ResourceAuthCert
// FillAuthCert 填充资源的授权凭证信息
// @param resources 实现了entity.IAuthCert接口的资源信息
FillAuthCert(authCerts []*entity.ResourceAuthCert, resources ...entity.IAuthCert)
}
type resourceAuthCertAppImpl struct {
base.AppImpl[*entity.ResourceAuthCert, repository.ResourceAuthCert]
tagTreeApp TagTree `inject:"TagTreeApp"`
}
// 注入Repo
func (r *resourceAuthCertAppImpl) InjectResourceAuthCertRepo(resourceAuthCertRepo repository.ResourceAuthCert) {
r.Repo = resourceAuthCertRepo
}
func (r *resourceAuthCertAppImpl) SaveAuthCert(ctx context.Context, params *SaveAuthCertParam) error {
resourceCode := params.ResourceCode
resourceType := int8(params.ResourceType)
resourceAuthCerts := params.AuthCerts
authCertTagType := params.AuthCertTagType
if authCertTagType == 0 {
return errorx.NewBiz("资源授权凭证所属标签类型不能为空")
}
if resourceCode == "" {
return errorx.NewBiz("资源授权凭证的资源编号不能为空")
}
// 删除授权信息
if len(resourceAuthCerts) == 0 {
if err := r.DeleteByCond(ctx, &entity.ResourceAuthCert{ResourceCode: resourceCode, ResourceType: resourceType}); err != nil {
return err
}
// 删除该资源下的所有授权凭证资源标签
if err := r.tagTreeApp.DeleteResource(ctx, &DelResourceTagParam{
ResourceCode: resourceCode,
ResourceType: params.ResourceType,
ChildType: authCertTagType,
}); err != nil {
return err
}
return nil
}
name2AuthCert := make(map[string]*entity.ResourceAuthCert, 0)
for _, resourceAuthCert := range resourceAuthCerts {
resourceAuthCert.ResourceCode = resourceCode
resourceAuthCert.ResourceType = int8(resourceType)
name2AuthCert[resourceAuthCert.Name] = resourceAuthCert
existNameAc := &entity.ResourceAuthCert{Name: resourceAuthCert.Name}
if r.GetBy(existNameAc) == nil && existNameAc.ResourceCode != resourceCode {
return errorx.NewBiz("授权凭证的名称不能重复[%s]", resourceAuthCert.Name)
}
// 公共授权凭证,则无需进行密文加密,密文即为公共授权凭证名
if resourceAuthCert.CiphertextType == entity.AuthCertCiphertextTypePublic {
continue
}
// 密文加密
if err := resourceAuthCert.CiphertextEncrypt(); err != nil {
return errorx.NewBiz(err.Error())
}
}
var oldAuthCert []*entity.ResourceAuthCert
r.ListByCond(&entity.ResourceAuthCert{ResourceCode: resourceCode, ResourceType: resourceType}, &oldAuthCert)
var adds, dels, unmodifys []string
if len(oldAuthCert) == 0 {
adds = collx.MapKeys(name2AuthCert)
} else {
oldNames := collx.ArrayMap(oldAuthCert, func(ac *entity.ResourceAuthCert) string {
return ac.Name
})
adds, dels, unmodifys = collx.ArrayCompare[string](collx.MapKeys(name2AuthCert), oldNames)
}
addAuthCerts := make([]*entity.ResourceAuthCert, 0)
for _, add := range adds {
addAc := name2AuthCert[add]
addAc.Id = 0
addAuthCerts = append(addAuthCerts, addAc)
}
// 处理新增的授权凭证
if len(addAuthCerts) > 0 {
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
})
// 保存授权凭证类型的资源标签
for _, authCert := range addAuthCerts {
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 {
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
}
}
for _, unmodify := range unmodifys {
unmodifyAc := name2AuthCert[unmodify]
if unmodifyAc.Id == 0 {
continue
}
if err := r.UpdateById(ctx, unmodifyAc); err != nil {
return err
}
}
return nil
}
func (r *resourceAuthCertAppImpl) GetAuthCert(authCertName string) (*entity.ResourceAuthCert, error) {
authCert := &entity.ResourceAuthCert{Name: authCertName}
if err := r.GetBy(authCert); err != nil {
return nil, errorx.NewBiz("该授权凭证不存在")
}
return r.decryptAuthCert(authCert)
}
func (r *resourceAuthCertAppImpl) GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error) {
var resourceAuthCerts []*entity.ResourceAuthCert
if err := r.ListByCond(&entity.ResourceAuthCert{
ResourceType: int8(resourceType),
ResourceCode: resourceCode,
}, &resourceAuthCerts); err != nil {
return nil, err
}
if len(resourceAuthCerts) == 0 {
return nil, errorx.NewBiz("该资源不存在授权凭证账号")
}
for _, resourceAuthCert := range resourceAuthCerts {
if resourceAuthCert.Type == entity.AuthCertTypePrivileged {
return r.decryptAuthCert(resourceAuthCert)
}
}
return r.decryptAuthCert(resourceAuthCerts[0])
}
func (r *resourceAuthCertAppImpl) GetAccountAuthCert(accountId uint64, authCertTagType entity.TagType, tagPath ...string) []*entity.ResourceAuthCert {
// 获取用户有权限操作的授权凭证资源标签
tagQuery := &entity.TagTreeQuery{
Type: authCertTagType,
CodePathLikes: tagPath,
}
authCertTags := r.tagTreeApp.GetAccountTagResources(accountId, tagQuery)
// 获取所有授权凭证名称
authCertNames := collx.ArrayMap(authCertTags, func(tag *entity.TagTree) string {
return tag.Code
})
var authCerts []*entity.ResourceAuthCert
r.GetRepo().ListByWheres(collx.M{
"name in ?": collx.ArrayDeduplicate(authCertNames),
}, &authCerts)
return authCerts
}
func (r *resourceAuthCertAppImpl) FillAuthCert(authCerts []*entity.ResourceAuthCert, resources ...entity.IAuthCert) {
if len(resources) == 0 {
return
}
// 资源编号 -> 资源
resourceCode2Resouce := collx.ArrayToMap(resources, func(ac entity.IAuthCert) string {
return ac.GetCode()
})
for _, authCert := range authCerts {
resourceCode2Resouce[authCert.ResourceCode].SetAuthCert(entity.AuthCert{
Name: authCert.Name,
Username: authCert.Username,
Type: authCert.Type,
CiphertextType: authCert.CiphertextType,
})
}
}
// 解密授权凭证信息
func (r *resourceAuthCertAppImpl) decryptAuthCert(authCert *entity.ResourceAuthCert) (*entity.ResourceAuthCert, error) {
if authCert.CiphertextType == entity.AuthCertCiphertextTypePublic {
// 如果是公共授权凭证,则密文为公共授权凭证名称,需要使用该名称再去获取对应的授权凭证
authCert = &entity.ResourceAuthCert{Name: authCert.Ciphertext}
if err := r.GetBy(authCert); err != nil {
return nil, errorx.NewBiz("该公共授权凭证[%s]不存在", authCert.Ciphertext)
}
}
if err := authCert.CiphertextDecrypt(); err != nil {
return nil, err
}
return authCert, nil
}

View File

@@ -16,9 +16,19 @@ import (
type SaveResourceTagParam struct {
ResourceCode string
ResourceName string
ResourceType int8
ResourceType entity.TagType
TagIds []uint64 // 关联标签,相当于父标签 pid
TagIds []uint64 // 关联标签,相当于父标签 pid,空数组则为删除该资源绑定的标签
}
type DelResourceTagParam struct {
ResourceCode string
ResourceType entity.TagType
Pid uint64 //父标签 pid
// 要删除的子节点类型,若存在值,则为删除资源标签下的指定类型的子标签
ChildType entity.TagType
}
type TagTree interface {
@@ -34,7 +44,7 @@ type TagTree interface {
// @param accountId 账号id
// @param resourceType 资源类型
// @param tagPath 访问指定的标签路径下关联的资源
GetAccountTagResources(accountId uint64, resourceType int8, tagPath string) []entity.TagTree
GetAccountTagResources(accountId uint64, query *entity.TagTreeQuery) []*entity.TagTree
// 获取指定账号有权限操作的资源codes
GetAccountResourceCodes(accountId uint64, resourceType int8, tagPath string) []string
@@ -42,6 +52,9 @@ type TagTree interface {
// SaveResource 保存资源标签
SaveResource(ctx context.Context, req *SaveResourceTagParam) error
// DeleteResource 删除资源标签,会删除该资源下所有子节点信息
DeleteResource(ctx context.Context, param *DelResourceTagParam) error
// 根据资源信息获取对应的标签路径列表
ListTagPathByResource(resourceType int8, resourceCode string) []string
@@ -115,12 +128,12 @@ func (p *tagTreeAppImpl) ListByQuery(condition *entity.TagTreeQuery, toEntity an
p.GetRepo().SelectByCondition(condition, toEntity)
}
func (p *tagTreeAppImpl) GetAccountTagResources(accountId uint64, resourceType int8, tagPath string) []entity.TagTree {
func (p *tagTreeAppImpl) GetAccountTagResources(accountId uint64, query *entity.TagTreeQuery) []*entity.TagTree {
tagResourceQuery := &entity.TagTreeQuery{
Type: resourceType,
Type: query.Type,
}
var tagResources []entity.TagTree
var tagResources []*entity.TagTree
var accountTagPaths []string
if accountId != consts.AdminId {
@@ -131,16 +144,37 @@ func (p *tagTreeAppImpl) GetAccountTagResources(accountId uint64, resourceType i
}
}
tagResourceQuery.CodePathLike = tagPath
// 去除空字符串标签
tagPaths := collx.ArrayRemoveBlank(query.CodePathLikes)
// 如果需要查询指定标签下的资源标签,则需要与用户拥有的权限进行过滤,避免越权
if len(tagPaths) > 0 {
// admin 则直接赋值需要获取的标签
if len(accountTagPaths) == 0 {
accountTagPaths = tagPaths
} else {
accountTagPaths = collx.ArrayFilter[string](tagPaths, func(s string) bool {
for _, v := range accountTagPaths {
// 要过滤的权限需要在用户拥有的子标签下, accountTagPath: test/ tagPath: test/test1/ -> true
if strings.HasPrefix(v, s) {
return true
}
}
return false
})
}
}
// tagResourceQuery.CodePathLike = tagPath
tagResourceQuery.Codes = query.Codes
tagResourceQuery.CodePathLikes = accountTagPaths
p.ListByQuery(tagResourceQuery, &tagResources)
return tagResources
}
func (p *tagTreeAppImpl) GetAccountResourceCodes(accountId uint64, resourceType int8, tagPath string) []string {
tagResources := p.GetAccountTagResources(accountId, resourceType, tagPath)
tagResources := p.GetAccountTagResources(accountId, &entity.TagTreeQuery{Type: entity.TagType(resourceType), CodePathLikes: []string{tagPath}})
// resouce code去重
code2Resource := collx.ArrayToMap[entity.TagTree, string](tagResources, func(val entity.TagTree) string {
code2Resource := collx.ArrayToMap[*entity.TagTree, string](tagResources, func(val *entity.TagTree) string {
return val.Code
})
@@ -149,7 +183,7 @@ func (p *tagTreeAppImpl) GetAccountResourceCodes(accountId uint64, resourceType
func (p *tagTreeAppImpl) SaveResource(ctx context.Context, req *SaveResourceTagParam) error {
resourceCode := req.ResourceCode
resourceType := req.ResourceType
resourceType := entity.TagType(req.ResourceType)
resourceName := req.ResourceName
tagIds := req.TagIds
@@ -162,7 +196,10 @@ func (p *tagTreeAppImpl) SaveResource(ctx context.Context, req *SaveResourceTagP
// 如果tagIds为空数组则为删除该资源标签
if len(tagIds) == 0 {
return p.DeleteByCond(ctx, &entity.TagTree{Code: resourceCode, Type: resourceType})
return p.DeleteResource(ctx, &DelResourceTagParam{
ResourceType: resourceType,
ResourceCode: resourceCode,
})
}
if resourceName == "" {
@@ -205,10 +242,36 @@ func (p *tagTreeAppImpl) SaveResource(ctx context.Context, req *SaveResourceTagP
if len(delTagIds) > 0 {
for _, tagId := range delTagIds {
cond := &entity.TagTree{Code: resourceCode, Type: resourceType, Pid: tagId}
if err := p.DeleteByCond(ctx, cond); err != nil {
if err := p.DeleteResource(ctx, &DelResourceTagParam{
ResourceType: resourceType,
ResourceCode: resourceCode,
Pid: tagId,
}); err != nil {
return err
}
}
}
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
}
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 {
return err
}
}
@@ -217,7 +280,7 @@ func (p *tagTreeAppImpl) SaveResource(ctx context.Context, req *SaveResourceTagP
func (p *tagTreeAppImpl) ListTagPathByResource(resourceType int8, resourceCode string) []string {
var trs []*entity.TagTree
p.ListByCond(&entity.TagTree{Type: resourceType, Code: resourceCode}, &trs)
p.ListByCond(&entity.TagTree{Type: entity.TagType(resourceType), Code: resourceCode}, &trs)
return collx.ArrayMap(trs, func(tr *entity.TagTree) string {
return tr.CodePath
})
@@ -260,7 +323,7 @@ func (p *tagTreeAppImpl) FillTagInfo(resources ...entity.ITagResource) {
for _, tr := range tagResources {
// 赋值标签信息
resourceCode2Resouce[tr.Code].SetTagInfo(entity.ResourceTag{TagId: tr.Pid, TagPath: tr.GetParentPath()})
resourceCode2Resouce[tr.Code].SetTagInfo(entity.ResourceTag{TagId: tr.Pid, TagPath: tr.GetParentPath(0)})
}
}

View File

@@ -6,8 +6,8 @@ type TagTreeQuery struct {
model.Model
Pid uint64
Type int8 `json:"type"`
Code string `json:"code"` // 标识
Type TagType `json:"type"`
Code string `json:"code"` // 标识
Codes []string
CodePath string `json:"codePath"` // 标识路径
CodePaths []string

View File

@@ -0,0 +1,123 @@
package entity
import (
"errors"
"mayfly-go/internal/common/utils"
"mayfly-go/pkg/model"
"github.com/may-fly/cast"
)
// 资源授权凭证
type ResourceAuthCert struct {
model.Model
Name string `json:"name"` // 名称(全局唯一)
ResourceCode string `json:"resourceCode"` // 资源编号
ResourceType int8 `json:"resourceType"` // 资源类型
Username string `json:"username"` // 用户名
Ciphertext string `json:"ciphertext"` // 密文
CiphertextType AuthCertCiphertextType `json:"ciphertextType"` // 密文类型
Extra model.Map[string, any] `json:"extra"` // 账号需要的其他额外信息(如秘钥口令等)
Type AuthCertType `json:"type"` // 凭证类型
Remark string `json:"remark"` // 备注
}
func (m *ResourceAuthCert) CiphertextEncrypt() error {
// 密码替换为加密后的密码
password, err := utils.PwdAesEncrypt(m.Ciphertext)
if err != nil {
return errors.New("加密密文失败")
}
m.Ciphertext = password
// 加密秘钥口令
if m.CiphertextType == AuthCertCiphertextTypePrivateKey {
passphrase := cast.ToString(m.Extra["passphrase"])
if passphrase != "" {
passphrase, err := utils.PwdAesEncrypt(passphrase)
if err != nil {
return errors.New("加密秘钥口令失败")
}
m.Extra["passphrase"] = passphrase
}
}
return nil
}
func (m *ResourceAuthCert) CiphertextDecrypt() error {
// 密码替换为解密后的密码
password, err := utils.PwdAesDecrypt(m.Ciphertext)
if err != nil {
return errors.New("解密密文失败")
}
m.Ciphertext = password
// 加密秘钥口令
if m.CiphertextType == AuthCertCiphertextTypePrivateKey {
passphrase := cast.ToString(m.Extra["passphrase"])
if passphrase != "" {
passphrase, err := utils.PwdAesDecrypt(passphrase)
if err != nil {
return errors.New("解密秘钥口令失败")
}
m.Extra["passphrase"] = passphrase
}
}
return nil
}
// 密文类型
type AuthCertCiphertextType int8
// 凭证类型
type AuthCertType int8
const (
AuthCertCiphertextTypePublic AuthCertCiphertextType = -1 // 公共授权凭证
AuthCertCiphertextTypePassword AuthCertCiphertextType = 1 // 密码
AuthCertCiphertextTypePrivateKey AuthCertCiphertextType = 2 // 私钥
AuthCertTypePublic AuthCertType = 2 // 公共凭证(可多个资源共享该授权凭证)
AuthCertTypePrivate AuthCertType = 1 // 普通私有凭证
AuthCertTypePrivileged AuthCertType = 11 // 特权私有凭证
AuthCertTypePrivateDefault AuthCertType = 12 // 默认私有凭证
)
// 授权凭证接口,填充资源授权凭证信息
type IAuthCert interface {
// 获取资源code
GetCode() string
// 设置授权信息
SetAuthCert(ac AuthCert)
}
// 资源关联的标签信息
type AuthCert struct {
Name string `json:"name" gorm:"-"` // 名称
Username string `json:"username" gorm:"-"` // 用户名
CiphertextType AuthCertCiphertextType `json:"ciphertextType" gorm:"-"` // 密文类型
Type AuthCertType `json:"type" gorm:"-"` // 凭证类型
}
func (r *AuthCert) SetAuthCert(ac AuthCert) {
r.Name = ac.Name
r.Username = ac.Username
r.Type = ac.Type
r.CiphertextType = ac.CiphertextType
}
// 资源标签列表
type AuthCerts struct {
AuthCerts []AuthCert `json:"authCerts" gorm:"-"`
}
func (r *AuthCerts) SetAuthCert(rt AuthCert) {
if r.AuthCerts == nil {
r.AuthCerts = make([]AuthCert, 0)
}
r.AuthCerts = append(r.AuthCerts, rt)
}

View File

@@ -1,6 +1,7 @@
package entity
import (
"mayfly-go/internal/common/consts"
"mayfly-go/pkg/model"
"strings"
)
@@ -9,17 +10,28 @@ import (
type TagTree struct {
model.Model
Pid uint64 `json:"pid"`
Type int8 `json:"type"` // 类型: -1.普通标签; 其他值则为对应的资源类型
Code string `json:"code"` // 标识编码, 若类型不为-1则为对应资源编码
CodePath string `json:"codePath"` // 标识路径
Name string `json:"name"` // 名称
Remark string `json:"remark"` // 备注说明
Pid uint64 `json:"pid"`
Type TagType `json:"type"` // 类型: -1.普通标签; 其他值则为对应的资源类型
Code string `json:"code"` // 标识编码, 若类型不为-1则为对应资源编码
CodePath string `json:"codePath"` // 标识路径
Name string `json:"name"` // 名称
Remark string `json:"remark"` // 备注说明
}
type TagType int8
const (
// 标识路径分隔符
CodePathSeparator = "/"
TagTypeTag TagType = -1
TagTypeMachine TagType = TagType(consts.TagResourceTypeMachine)
TagTypeDb TagType = TagType(consts.TagResourceTypeDb)
TagTypeRedis TagType = TagType(consts.TagResourceTypeRedis)
TagTypeMongo TagType = TagType(consts.TagResourceTypeMongo)
TagTypeMachineAuthCert TagType = 11 // 机器-授权凭证
TagTypeDbAuthCert TagType = 21 // DB-授权凭证
)
// GetRootCode 获取根路径信息
@@ -27,19 +39,25 @@ func (pt *TagTree) GetRootCode() string {
return strings.Split(pt.CodePath, CodePathSeparator)[0]
}
// GetParentPath 获取父标签路径, 如CodePath = test/test1/test2/ -> test/test1/
func (pt *TagTree) GetParentPath() string {
// 去末尾的分隔符
input := strings.TrimRight(pt.CodePath, CodePathSeparator)
// GetParentPath 获取父标签路径, 如CodePath = test/test1/test2/ -> index = 0 => test/test1/ index = 1 => test/
func (pt *TagTree) GetParentPath(index int) string {
// 去末尾的斜杠
codePath := strings.TrimSuffix(pt.CodePath, "/")
// 查找倒数第二个连字符位置
lastHyphenIndex := strings.LastIndex(input, CodePathSeparator)
if lastHyphenIndex == -1 {
return ""
// 使用 Split 方法将路径按斜杠分割成切片
paths := strings.Split(codePath, "/")
// 确保索引在有效范围内
if index < 0 {
index = 0
} else if index > len(paths)-2 {
index = len(paths) - 2
}
// 截取字符串
return input[:lastHyphenIndex+1]
// 按索引拼接父标签路径
parentPath := strings.Join(paths[:len(paths)-index-1], "/")
return parentPath + "/"
}
// 标签接口资源,如果要实现资源结构体填充标签信息,则资源结构体需要实现该接口

View File

@@ -0,0 +1,10 @@
package repository
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
)
type ResourceAuthCert interface {
base.Repo[*entity.ResourceAuthCert]
}

View File

@@ -9,4 +9,5 @@ func InitIoc() {
ioc.Register(newTagTreeTeamRepo(), ioc.WithComponentName("TagTreeTeamRepo"))
ioc.Register(newTeamRepo(), ioc.WithComponentName("TeamRepo"))
ioc.Register(newTeamMemberRepo(), ioc.WithComponentName("TeamMemberRepo"))
ioc.Register(newResourceAuthCertRepoImpl(), ioc.WithComponentName("ResourceAuthCertRepo"))
}

View File

@@ -0,0 +1,15 @@
package persistence
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
)
type resourceAuthCertRepoImpl struct {
base.RepoImpl[*entity.ResourceAuthCert]
}
func newResourceAuthCertRepoImpl() repository.ResourceAuthCert {
return &resourceAuthCertRepoImpl{base.RepoImpl[*entity.ResourceAuthCert]{M: new(entity.ResourceAuthCert)}}
}

View File

@@ -0,0 +1,24 @@
package router
import (
"mayfly-go/internal/tag/api"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func InitResourceAuthCertRouter(router *gin.RouterGroup) {
m := new(api.ResourceAuthCert)
biz.ErrIsNil(ioc.Inject(m))
resourceAuthCert := router.Group("/auth-certs")
{
reqs := [...]*req.Conf{
req.NewGet("", m.ListByQuery),
}
req.BatchSetGroup(resourceAuthCert, reqs[:])
}
}

View File

@@ -5,4 +5,5 @@ import "github.com/gin-gonic/gin"
func Init(router *gin.RouterGroup) {
InitTagTreeRouter(router)
InitTeamRouter(router)
InitResourceAuthCertRouter(router)
}

View File

@@ -6,6 +6,7 @@ import (
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/global"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
"gorm.io/gorm"
)
@@ -31,12 +32,28 @@ type App[T model.ModelI] interface {
// 使用指定gorm db执行主要用于事务执行
UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T) error
// UpdateByWheres 更新满足wheres条件的数据
// @param wheres key => "age > ?" value => 10等
UpdateByWheres(ctx context.Context, e T, wheres collx.M, columns ...string) error
// UpdateByWheresWithDb 使用指定gorm.Db更新满足wheres条件的数据
// @param wheres key => "age > ?" value => 10等
UpdateByWheresWithDb(ctx context.Context, db *gorm.DB, e T, wheres collx.M, columns ...string) error
// 根据实体主键删除实体
DeleteById(ctx context.Context, id uint64) error
// 使用指定gorm db执行主要用于事务执行
DeleteByIdWithDb(ctx context.Context, db *gorm.DB, id uint64) error
// DeleteByWheres 根据wheres条件进行删除
// @param wheres key -> "age > ?" value -> 10等
DeleteByWheres(ctx context.Context, wheres collx.M) error
// DeleteByWheresWithDb 使用指定gorm.Db根据wheres条件进行删除
// @param wheres key -> "age > ?" value -> 10等
DeleteByWheresWithDb(ctx context.Context, db *gorm.DB, wheres collx.M) error
// 根据实体条件更新参数udpateFields指定字段
Updates(ctx context.Context, cond any, udpateFields map[string]any) error
@@ -64,6 +81,9 @@ type App[T model.ModelI] interface {
// 根据条件查询数据映射至listModels
ListByCond(cond any, listModels any, cols ...string) error
// PageQuery 分页查询
PageQuery(cond any, pageParam *model.PageParam, toModels any) (*model.PageResult[any], error)
// 获取满足model中不为空的字段值条件的所有数据.
//
// @param list为数组类型 如 var users *[]User可指定为非model结构体
@@ -117,6 +137,14 @@ func (ai *AppImpl[T, R]) UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T)
return ai.GetRepo().UpdateByIdWithDb(ctx, db, e)
}
func (ai *AppImpl[T, R]) UpdateByWheres(ctx context.Context, e T, wheres collx.M, columns ...string) error {
return ai.GetRepo().UpdateByWheres(ctx, e, wheres, columns...)
}
func (ai *AppImpl[T, R]) UpdateByWheresWithDb(ctx context.Context, db *gorm.DB, e T, wheres collx.M, columns ...string) error {
return ai.GetRepo().UpdateByWheresWithDb(ctx, db, e, wheres, columns...)
}
// 根据实体条件更新参数udpateFields指定字段 (单纯更新,不做其他业务逻辑处理)
func (ai *AppImpl[T, R]) Updates(ctx context.Context, cond any, udpateFields map[string]any) error {
return ai.GetRepo().Updates(cond, udpateFields)
@@ -152,6 +180,14 @@ func (ai *AppImpl[T, R]) DeleteByCondWithDb(ctx context.Context, db *gorm.DB, co
return ai.GetRepo().DeleteByCondWithDb(ctx, db, cond)
}
func (ai *AppImpl[T, R]) DeleteByWheres(ctx context.Context, wheres collx.M) error {
return ai.GetRepo().DeleteByWheres(ctx, wheres)
}
func (ai *AppImpl[T, R]) DeleteByWheresWithDb(ctx context.Context, db *gorm.DB, wheres collx.M) error {
return ai.GetRepo().DeleteByWheresWithDb(ctx, db, wheres)
}
// 根据实体id查询
func (ai *AppImpl[T, R]) GetById(e T, id uint64, cols ...string) (T, error) {
if err := ai.GetRepo().GetById(e, id, cols...); err != nil {
@@ -174,6 +210,11 @@ func (ai *AppImpl[T, R]) ListByCond(cond any, listModels any, cols ...string) er
return ai.GetRepo().ListByCond(cond, listModels, cols...)
}
// PageQuery 分页查询
func (ai *AppImpl[T, R]) PageQuery(cond any, pageParam *model.PageParam, toModels any) (*model.PageResult[any], error) {
return ai.GetRepo().PageQuery(cond, pageParam, toModels)
}
// 获取满足model中不为空的字段值条件的所有数据.
//
// @param list为数组类型 如 var users *[]User可指定为非model结构体

View File

@@ -5,6 +5,7 @@ import (
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/gormx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
"gorm.io/gorm"
)
@@ -33,6 +34,14 @@ type Repo[T model.ModelI] interface {
// 使用指定gorm db执行主要用于事务执行
UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T, columns ...string) error
// UpdateByWheres 更新满足wheres条件的数据
// @param wheres key => "age > ?" value => 10等
UpdateByWheres(ctx context.Context, e T, wheres collx.M, columns ...string) error
// UpdateByWheresWithDb 使用指定gorm.Db更新满足wheres条件的数据
// @param wheres key => "age > ?" value => 10等
UpdateByWheresWithDb(ctx context.Context, db *gorm.DB, e T, wheres collx.M, columns ...string) error
// 保存实体实体IsCreate返回true则新增否则更新
Save(ctx context.Context, e T) error
@@ -55,6 +64,14 @@ type Repo[T model.ModelI] interface {
// 使用指定gorm db执行主要用于事务执行
DeleteByCondWithDb(ctx context.Context, db *gorm.DB, cond any) error
// DeleteByWheres 根据wheres条件进行删除
// @param wheres key -> "age > ?" value -> 10等
DeleteByWheres(ctx context.Context, wheres collx.M) error
// DeleteByWheresWithDb 使用指定gorm.Db根据wheres条件进行删除
// @param wheres key -> "age > ?" value -> 10等
DeleteByWheresWithDb(ctx context.Context, db *gorm.DB, wheres collx.M) error
// 根据实体id查询
GetById(e T, id uint64, cols ...string) error
@@ -67,6 +84,13 @@ type Repo[T model.ModelI] interface {
// 根据实体条件查询数据映射至listModels
ListByCond(cond any, listModels any, cols ...string) error
// 根据wheres条件进行过滤
// @param wheres key -> "age > ?" value -> 10等
ListByWheres(wheres collx.M, listModels any, cols ...string) error
// PageQuery 分页查询
PageQuery(cond any, pageParam *model.PageParam, toModels any) (*model.PageResult[any], error)
// 获取满足model中不为空的字段值条件的所有数据.
//
// @param list为数组类型 如 var users *[]User可指定为非model结构体
@@ -123,6 +147,24 @@ func (br *RepoImpl[T]) UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T, c
return gormx.UpdateByIdWithDb(db, br.fillBaseInfo(ctx, e), columns...)
}
func (br *RepoImpl[T]) UpdateByWheres(ctx context.Context, e T, wheres collx.M, columns ...string) error {
if db := contextx.GetDb(ctx); db != nil {
return br.UpdateByWheresWithDb(ctx, db, e, wheres, columns...)
}
e = br.fillBaseInfo(ctx, e)
// model的主键值需为空否则会带上主键条件
e.SetId(0)
return gormx.UpdateByWheres(e, wheres)
}
func (br *RepoImpl[T]) UpdateByWheresWithDb(ctx context.Context, db *gorm.DB, e T, wheres collx.M, columns ...string) error {
e = br.fillBaseInfo(ctx, e)
// model的主键值需为空否则会带上主键条件
e.SetId(0)
return gormx.UpdateByWheresWithDb(db, br.fillBaseInfo(ctx, e), wheres, columns...)
}
func (br *RepoImpl[T]) Updates(cond any, udpateFields map[string]any) error {
return gormx.Updates(br.GetModel(), cond, udpateFields)
}
@@ -163,6 +205,23 @@ func (br *RepoImpl[T]) DeleteByCondWithDb(ctx context.Context, db *gorm.DB, cond
return gormx.DeleteByCondWithDb(db, br.GetModel(), cond)
}
func (br *RepoImpl[T]) DeleteByWheres(ctx context.Context, wheres collx.M) error {
if db := contextx.GetDb(ctx); db != nil {
return br.DeleteByWheresWithDb(ctx, db, wheres)
}
// model的主键值需为空否则会带上主键条件
e := br.GetModel()
e.SetId(0)
return gormx.DeleteByWheres(e, wheres)
}
func (br *RepoImpl[T]) DeleteByWheresWithDb(ctx context.Context, db *gorm.DB, wheres collx.M) error {
// model的主键值需为空否则会带上主键条件
e := br.GetModel()
e.SetId(0)
return gormx.DeleteByWheresWithDb(db, e, wheres)
}
func (br *RepoImpl[T]) GetById(e T, id uint64, cols ...string) error {
if err := gormx.GetById(e, id, cols...); err != nil {
return err
@@ -182,6 +241,15 @@ func (br *RepoImpl[T]) ListByCond(cond any, listModels any, cols ...string) erro
return gormx.ListByCond(br.GetModel(), cond, listModels, cols...)
}
func (br *RepoImpl[T]) ListByWheres(wheres collx.M, listModels any, cols ...string) error {
return gormx.ListByWheres(br.GetModel(), wheres, listModels, cols...)
}
func (br *RepoImpl[T]) PageQuery(cond any, pageParam *model.PageParam, toModels any) (*model.PageResult[any], error) {
qd := gormx.NewQuery(br.GetModel()).WithCondModel(cond)
return gormx.PageQuery(qd, pageParam, toModels)
}
func (br *RepoImpl[T]) ListByCondOrder(cond any, list any, order ...string) error {
return gormx.ListByCondOrder(br.GetModel(), cond, list, order...)
}

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"mayfly-go/pkg/global"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx"
"strings"
"time"
@@ -93,6 +95,19 @@ func ListByCond(model any, cond any, list any, cols ...string) error {
return global.Db.Model(model).Select(cols).Where(cond).Scopes(UndeleteScope).Order("id desc").Find(list).Error
}
// 获取满足cond中不为空的字段值条件的所有model表数据.
//
// @param wheres key -> "age > ?" value -> 10等
func ListByWheres(model any, wheres collx.M, list any, cols ...string) error {
gdb := global.Db.Model(model).Select(cols)
for k, v := range wheres {
if !anyx.IsBlank(v) {
gdb.Where(k, v)
}
}
return gdb.Scopes(UndeleteScope).Order("id desc").Find(list).Error
}
// 获取满足model中不为空的字段值条件的所有数据.
//
// @param list为数组类型 如 var users *[]User可指定为非model结构体
@@ -154,6 +169,24 @@ func UpdateByIdWithDb(db *gorm.DB, model any, columns ...string) error {
return db.Model(model).Select(columns).Updates(model).Error
}
// UpdateByWheres 更新满足wheres条件的数据(model的主键值需为空否则会带上主键条件)
// @param wheres key -> "age > ?" value -> 10等
func UpdateByWheres(model_ any, wheres collx.M) error {
return UpdateByWheresWithDb(global.Db, model_, wheres)
}
// UpdateByWheresWithDb 使用指定gorm.DB更新满足wheres条件的数据(model的主键值需为空否则会带上主键条件)
// @param wheres key -> "age > ?" value -> 10等
func UpdateByWheresWithDb(db *gorm.DB, model any, wheres collx.M, columns ...string) error {
gormDb := db.Model(model).Select(columns)
for k, v := range wheres {
if !anyx.IsBlank(v) {
gormDb.Where(k, v)
}
}
return gormDb.Updates(model).Error
}
// 根据实体条件更新参数udpateFields指定字段
func Updates(model any, condition any, updateFields map[string]any) error {
return global.Db.Model(model).Where(condition).Updates(updateFields).Error
@@ -190,6 +223,24 @@ func DeleteByWithDb(db *gorm.DB, model_ any) error {
return DeleteByCondWithDb(db, model_, model_)
}
// DeleteByWheres 使用指定wheres删除(model的主键值需为空否则会带上主键条件)
// @param wheres key -> "age > ?" value -> 10等
func DeleteByWheres(model_ any, wheres collx.M) error {
return DeleteByWheresWithDb(global.Db, model_, wheres)
}
// DeleteByWheresWithDb 使用指定gorm.Db根据wheres条件进行删除(model的主键值需为空否则会带上主键条件)
// @param wheres key -> "age > ?" value -> 10等
func DeleteByWheresWithDb(db *gorm.DB, model_ any, wheres collx.M) error {
gormDb := db.Model(model_)
for k, v := range wheres {
if !anyx.IsBlank(v) {
gormDb.Where(k, v)
}
}
return gormDb.Updates(getDeleteColumnValue()).Error
}
// 根据cond条件删除指定model表数据
//
// @param model 数据库映射实体模型

View File

@@ -22,11 +22,13 @@ const (
// 实体接口
type ModelI interface {
// SetId 设置id
SetId(id uint64)
// 是否为新建该实体模型, 默认 id == 0 为新建
// IsCreate 是否为新建该实体模型, 默认 id == 0 为新建
IsCreate() bool
// 使用当前登录账号信息赋值实体结构体的基础信息
// FillBaseInfo 使用当前登录账号信息赋值实体结构体的基础信息
//
// 如创建时间,修改时间,创建者,修改者信息等
FillBaseInfo(idGenType IdGenType, account *LoginAccount)
@@ -36,6 +38,10 @@ type IdModel struct {
Id uint64 `json:"id"`
}
func (m *IdModel) SetId(id uint64) {
m.Id = id
}
func (m *IdModel) IsCreate() bool {
return m.Id == 0
}
@@ -45,7 +51,7 @@ func (m *IdModel) FillBaseInfo(idGenType IdGenType, account *LoginAccount) {
if !m.IsCreate() {
return
}
m.Id = GetIdByGenType(idGenType)
m.SetId(GetIdByGenType(idGenType))
}
// 含有删除字段模型

View File

@@ -1,6 +1,9 @@
package collx
import "strings"
import (
"mayfly-go/pkg/utils/anyx"
"strings"
)
// 数组比较
// 依次返回,新增值,删除值,以及不变值
@@ -131,6 +134,13 @@ func ArrayRemoveFunc[T any](arr []T, isDeleteFunc func(T) bool) []T {
return newArr
}
// ArrayRemoveBlank 移除元素中的空元素
func ArrayRemoveBlank[T any](arr []T) []T {
return ArrayRemoveFunc(arr, func(val T) bool {
return anyx.IsBlank(val)
})
}
// 数组元素去重
func ArrayDeduplicate[T comparable](arr []T) []T {
encountered := map[T]bool{}
@@ -155,3 +165,14 @@ func ArrayAnyMatches(arr []string, subStr string) bool {
}
return false
}
// ArrayFilter 过滤函数,根据提供的条件函数将切片中的元素进行过滤
func ArrayFilter[T any](array []T, fn func(T) bool) []T {
var filtered []T
for _, val := range array {
if fn(val) {
filtered = append(filtered, val)
}
}
return filtered
}

View File

@@ -335,10 +335,6 @@ CREATE TABLE `t_machine` (
`ip` varchar(50) NOT NULL,
`port` int(12) NOT NULL,
`protocol` tinyint(2) NULL COMMENT '协议 1、SSH 2、RDP',
`username` varchar(12) NOT NULL,
`auth_method` tinyint(2) DEFAULT NULL COMMENT '1.密码登录2.publickey登录',
`password` varchar(100) DEFAULT NULL,
`auth_cert_id` bigint(20) DEFAULT NULL COMMENT '授权凭证id',
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
`enable_recorder` tinyint(2) DEFAULT NULL COMMENT '是否启用终端回放记录',
`status` tinyint(2) NOT NULL COMMENT '状态: 1:启用; -1:禁用',
@@ -893,6 +889,7 @@ DROP TABLE IF EXISTS `t_tag_tree`;
CREATE TABLE `t_tag_tree` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`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 '标识符路径',
`name` varchar(36) DEFAULT NULL COMMENT '名称',
@@ -997,6 +994,31 @@ BEGIN;
INSERT INTO `t_team_member` VALUES (7, 3, 1, 'admin', '2022-10-26 20:04:36', 1, 'admin', '2022-10-26 20:04:36', 1, 'admin', 0, NULL);
COMMIT;
DROP TABLE IF EXISTS `t_resource_auth_cert`;
-- 资源授权凭证
CREATE TABLE `t_resource_auth_cert` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号名称',
`resource_code` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '资源编码',
`resource_type` tinyint NOT NULL COMMENT '资源类型',
`type` tinyint DEFAULT NULL COMMENT '凭证类型',
`username` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',
`ciphertext` varchar(5000) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密文内容',
`ciphertext_type` tinyint NOT NULL COMMENT '密文类型(-1.公共授权凭证 1.密码 2.秘钥)',
`extra` varchar(200) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号需要的其他额外信息(如秘钥口令等)',
`remark` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL,
`creator_id` bigint NOT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint NOT NULL,
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`is_deleted` tinyint DEFAULT '0',
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_resource_code` (`resource_code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='资源授权凭证表';
DROP TABLE IF EXISTS `t_flow_procdef`;
-- 工单流程相关表
CREATE TABLE `t_flow_procdef` (

View File

@@ -55,6 +55,9 @@ update `t_machine` set `protocol` = 1 where `protocol` is NULL;
delete from `t_sys_config` where `key` = 'MachineConfig';
INSERT INTO t_sys_config ( name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('机器相关配置', 'MachineConfig', '[{"name":"终端回放存储路径","model":"terminalRecPath","placeholder":"终端回放存储路径"},{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB、2GB等)"},{"model":"termOpSaveDays","name":"终端记录保存时间","placeholder":"终端记录保存时间(单位天)"},{"model":"guacdHost","name":"guacd服务ip","placeholder":"guacd服务ip默认 127.0.0.1","required":false},{"name":"guacd服务端口","model":"guacdPort","placeholder":"guacd服务端口默认 4822","required":false},{"model":"guacdFilePath","name":"guacd服务文件存储位置","placeholder":"guacd服务文件存储位置用于挂载RDP文件夹"},{"name":"guacd服务记录存储位置","model":"guacdRecPath","placeholder":"guacd服务记录存储位置用于记录rdp操作记录"}]', '{"terminalRecPath":"./rec","uploadMaxFileSize":"1000MB","termOpSaveDays":"30","guacdHost":"","guacdPort":"","guacdFilePath":"./guacd/rdp-file","guacdRecPath":"./guacd/rdp-rec"}', '机器相关配置,如终端回放路径等', 'all', '2023-07-13 16:26:44', 1, 'admin', '2024-04-06 12:25:03', 1, 'admin', 0, NULL);
ALTER TABLE t_tag_tree ADD `type` tinyint NOT NULL DEFAULT '-1' COMMENT '类型: -1.普通标签; 其他值则为对应的资源类型';
BEGIN;
INSERT
INTO
@@ -88,5 +91,34 @@ from
WHERE
is_deleted = 0;
DROP TABLE t_tag_tree;
COMMIT;
DROP TABLE t_tag_resource;
COMMIT;
-- 资源授权凭证
CREATE TABLE `t_resource_auth_cert` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号名称',
`resource_code` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '资源编码',
`resource_type` tinyint NOT NULL COMMENT '资源类型',
`type` tinyint DEFAULT NULL COMMENT '凭证类型',
`username` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',
`ciphertext` varchar(5000) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密文内容',
`ciphertext_type` tinyint NOT NULL COMMENT '密文类型(-1.公共授权凭证 1.密码 2.秘钥)',
`extra` varchar(200) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号需要的其他额外信息(如秘钥口令等)',
`remark` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL,
`creator_id` bigint NOT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint NOT NULL,
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`is_deleted` tinyint DEFAULT '0',
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_resource_code` (`resource_code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='资源授权凭证表';
-- 删除机器表 账号相关字段
ALTER TABLE t_machine DROP COLUMN username;
ALTER TABLE t_machine DROP COLUMN password;
ALTER TABLE t_machine DROP COLUMN auth_cert_id;