feat: Support authkeys management (#10480)

Refs #5247
This commit is contained in:
ssongliu
2025-09-25 07:26:10 +00:00
committed by GitHub
parent 6b24ee23e6
commit 280c1b4fea
16 changed files with 189 additions and 63 deletions
+32 -25
View File
@@ -67,28 +67,6 @@ func (b *BaseApi) UpdateSSH(c *gin.Context) {
helper.Success(c)
}
// @Tags SSH
// @Summary Update host SSH setting by file
// @Accept json
// @Param request body dto.SSHConf true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/ssh/conffile/update [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件","formatEN":"update SSH conf"}
func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
var req dto.SSHConf
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := sshService.UpdateByFile(req.File); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags SSH
// @Summary Generate host SSH secret
// @Accept json
@@ -246,15 +224,44 @@ func (b *BaseApi) ExportSSHLogs(c *gin.Context) {
// @Tags SSH
// @Summary Load host SSH conf
// @Accept json
// @Param request body dto.OperationWithName true "request"
// @Success 200 {string} conf
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/ssh/conf [get]
func (b *BaseApi) LoadSSHConf(c *gin.Context) {
data, err := sshService.LoadSSHConf()
// @Router /hosts/ssh/file [post]
func (b *BaseApi) LoadSSHFile(c *gin.Context) {
var req dto.OperationWithName
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := sshService.LoadSSHFile(req.Name)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags SSH
// @Summary Update host SSH setting by file
// @Accept json
// @Param request body dto.SSHConf true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/ssh/file/update [post]
// @x-panel-log {"bodyKeys":["key"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件 [key]","formatEN":"update SSH conf [key]"}
func (b *BaseApi) UpdateSSHByFile(c *gin.Context) {
var req dto.SettingUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := sshService.UpdateByFile(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+53 -25
View File
@@ -38,9 +38,9 @@ type SSHService struct{}
type ISSHService interface {
GetSSHInfo() (*dto.SSHInfo, error)
OperateSSH(operation string) error
UpdateByFile(value string) error
Update(req dto.SSHUpdate) error
LoadSSHConf() (string, error)
LoadSSHFile(name string) (string, error)
UpdateByFile(req dto.SettingUpdate) error
LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error)
ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error)
@@ -223,25 +223,6 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
return nil
}
func (u *SSHService) UpdateByFile(value string) error {
serviceName, err := loadServiceName()
if err != nil {
return err
}
file, err := os.OpenFile(sshPath, os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
if err != nil {
return err
}
defer file.Close()
if _, err = file.WriteString(value); err != nil {
return err
}
sudo := cmd.SudoHandleCmd()
_, _ = cmd.RunDefaultWithStdoutBashCf("%s systemctl restart %s", sudo, serviceName)
return nil
}
func (u *SSHService) SyncRootCert() error {
currentUser, err := user.Current()
if err != nil {
@@ -500,17 +481,64 @@ func (u *SSHService) ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string,
return tmpFileName, nil
}
func (u *SSHService) LoadSSHConf() (string, error) {
if _, err := os.Stat("/etc/ssh/sshd_config"); err != nil {
return "", buserr.New("ErrHttpReqNotFound")
func (u *SSHService) LoadSSHFile(name string) (string, error) {
var fileName string
switch name {
case "authKeys":
currentUser, err := user.Current()
if err != nil {
return "", fmt.Errorf("load current user failed, err: %v", err)
}
fileName = currentUser.HomeDir + "/.ssh/authorized_keys"
case "sshdConf":
fileName = "/etc/ssh/sshd_config"
default:
return "", buserr.WithName("ErrNotSupportType", name)
}
content, err := os.ReadFile("/etc/ssh/sshd_config")
if _, err := os.Stat(fileName); err != nil {
return "", buserr.WithErr("ErrHttpReqNotFound", err)
}
content, err := os.ReadFile(fileName)
if err != nil {
return "", err
}
return string(content), nil
}
func (u *SSHService) UpdateByFile(req dto.SettingUpdate) error {
var fileName string
switch req.Key {
case "authKeys":
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("load current user failed, err: %v", err)
}
fileName = currentUser.HomeDir + "/.ssh/authorized_keys"
case "sshdConf":
fileName = "/etc/ssh/sshd_config"
default:
return buserr.WithName("ErrNotSupportType", req.Key)
}
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
if err != nil {
return err
}
defer file.Close()
if _, err = file.WriteString(req.Value); err != nil {
return err
}
if req.Key == "authKeys" {
return nil
}
serviceName, err := loadServiceName()
if err != nil {
return err
}
sudo := cmd.SudoHandleCmd()
_, _ = cmd.RunDefaultWithStdoutBashCf("%s systemctl restart %s", sudo, serviceName)
return nil
}
func sortFileList(fileNames []sshFileItem) []sshFileItem {
if len(fileNames) < 2 {
return fileNames
+2 -2
View File
@@ -29,13 +29,13 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.GET("/monitor/setting", baseApi.LoadMonitorSetting)
hostRouter.POST("/monitor/setting/update", baseApi.UpdateMonitorSetting)
hostRouter.GET("/ssh/conf", baseApi.LoadSSHConf)
hostRouter.POST("/ssh/search", baseApi.GetSSHInfo)
hostRouter.POST("/ssh/update", baseApi.UpdateSSH)
hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs)
hostRouter.POST("/ssh/log/export", baseApi.ExportSSHLogs)
hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile)
hostRouter.POST("/ssh/operate", baseApi.OperateSSH)
hostRouter.POST("/ssh/file", baseApi.LoadSSHFile)
hostRouter.POST("/ssh/file/update", baseApi.UpdateSSHByFile)
hostRouter.POST("/ssh/cert", baseApi.CreateRootCert)
hostRouter.POST("/ssh/cert/sync", baseApi.SyncRootCert)
+5 -5
View File
@@ -68,17 +68,17 @@ export const updateMonitorSetting = (key: string, value: string) => {
export const getSSHInfo = () => {
return http.post<Host.SSHInfo>(`/hosts/ssh/search`);
};
export const getSSHConf = () => {
return http.get<string>(`/hosts/ssh/conf`);
};
export const operateSSH = (operation: string) => {
return http.post(`/hosts/ssh/operate`, { operation: operation }, TimeoutEnum.T_40S);
};
export const updateSSH = (params: Host.SSHUpdate) => {
return http.post(`/hosts/ssh/update`, params, TimeoutEnum.T_40S);
};
export const updateSSHByfile = (file: string) => {
return http.post(`/hosts/ssh/conffile/update`, { file: file }, TimeoutEnum.T_40S);
export const loadSSHFile = (name: string) => {
return http.post<string>(`/hosts/ssh/file`, { name: name });
};
export const updateSSHByFile = (key: string, file: string) => {
return http.post(`/hosts/ssh/file/update`, { key: key, value: file }, TimeoutEnum.T_60S);
};
export const createCert = (params: Host.RootCert) => {
let request = deepCopy(params) as Host.RootCert;
+2
View File
@@ -1576,6 +1576,8 @@ const message = {
syncHelper: 'The sync operation will clean invalid keys and sync new complete key pairs. Continue?',
input: 'Manual Input',
import: 'File Upload',
authKeys: 'Public Key Management',
authKeysHelper: 'Save current public key information?',
pubkey: 'Key info',
pubKeyHelper: 'The current key information only takes effect for user {0}',
encryptionMode: 'Encryption mode',
+2
View File
@@ -1572,6 +1572,8 @@ const message = {
'La operación de sincronización limpiará las claves inválidas y sincronizará nuevos pares de claves completos. ¿Desea continuar?',
input: 'Entrada manual',
import: 'Subir archivo',
authKeys: 'Gestión de Claves Públicas',
authKeysHelper: '¿Guardar información actual de clave pública?',
pubkey: 'Información de clave',
pubKeyHelper: 'La información de la clave actual solo tiene efecto para el usuario {0}',
encryptionMode: 'Modo de cifrado',
+2
View File
@@ -1523,6 +1523,8 @@ const message = {
syncHelper: '同期操作は無効なキーをクリーンアップし、新しい完全なキーペアを同期します。続行しますか?',
input: '手動入力',
import: 'ファイルアップロード',
authKeys: '公開鍵管理',
authKeysHelper: '現在の公開鍵情報を保存しますか?',
pubkey: '重要な情報',
encryptionMode: '暗号化モード',
pubKeyHelper: '現在の鍵情報はユーザー {0} にのみ有効です',
+2
View File
@@ -1506,6 +1506,8 @@ const message = {
syncHelper: '동기화 작업으로 유효하지 않은 키를 정리하고 새로운 완전한 키 쌍을 동기화합니다. 계속하시겠습니까?',
input: '수동 입력',
import: '파일 업로드',
authKeys: '공개 키 관리',
authKeysHelper: '현재 공개 키 정보를 저장하시겠습니까?',
pubkey: '키 정보',
encryptionMode: '암호화 모드',
pubKeyHelper: '현재 키 정보는 사용자 {0}에게만 적용됩니다',
+2
View File
@@ -1565,6 +1565,8 @@ const message = {
'Operasi segerak akan membersihkan kunci tidak sah dan menyegerakkan pasangan kunci baru yang lengkap. Teruskan?',
input: 'Input Manual',
import: 'Muat Naik Fail',
authKeys: 'Pengurusan Kunci Awam',
authKeysHelper: 'Simpan maklumat kunci awam semasa?',
pubkey: 'Maklumat kunci',
pubKeyHelper: 'Maklumat kunci semasa hanya berkuat kuasa untuk pengguna {0}',
encryptionMode: 'Mod penyulitan',
+2
View File
@@ -1554,6 +1554,8 @@ const message = {
'A operação de sincronização limpará chaves inválidas e sincronizará novos pares de chaves completos. Continuar?',
input: 'Entrada Manual',
import: 'Upload de Arquivo',
authKeys: 'Gerenciamento de Chaves Públicas',
authKeysHelper: 'Salvar informações atuais da chave pública?',
pubkey: 'Informações da chave',
pubKeyHelper: 'A informação da chave atual só tem efeito para o usuário {0}',
encryptionMode: 'Modo de criptografia',
+2
View File
@@ -1556,6 +1556,8 @@ const message = {
'Операция синхронизации удалит недействительные ключи и синхронизирует новые полные ключевые пары. Продолжить?',
input: 'Ручной ввод',
import: 'Загрузка файла',
authKeys: 'Управление Открытыми Ключами',
authKeysHelper: 'Сохранить текущую информацию об открытом ключе?',
pubkey: 'Информация о ключе',
pubKeyHelper: 'Текущая информация о ключе действительна только для пользователя {0}',
encryptionMode: 'Режим шифрования',
+2
View File
@@ -1594,6 +1594,8 @@ const message = {
'Eşitleme işlemi geçersiz anahtarları temizleyecek ve yeni tam anahtar çiftlerini eşitleyecek. Devam edilsin mi?',
input: 'Manuel Giriş',
import: 'Dosya Yükleme',
authKeys: 'Ortak Anahtar Yönetimi',
authKeysHelper: 'Mevcut ortak anahtar bilgilerini kaydet?',
pubkey: 'Anahtar bilgisi',
pubKeyHelper: 'Mevcut anahtar bilgileri yalnızca {0} kullanıcısı için geçerlidir',
encryptionMode: 'Şifreleme modu',
+2
View File
@@ -1500,6 +1500,8 @@ const message = {
syncHelper: '同步操作將清理失效金鑰並同步新增的完整金鑰對,是否繼續?',
input: '手動輸入',
import: '文件上傳',
authKeys: '公鑰管理',
authKeysHelper: '是否儲存目前公鑰資訊?',
pubkey: '金鑰資訊',
pubKeyHelper: '目前金鑰資訊僅對使用者 {0} 生效',
encryptionMode: '加密方式',
+2
View File
@@ -1496,6 +1496,8 @@ const message = {
syncHelper: '同步操作将清理失效密钥并同步新增的完整密钥对,是否继续?',
input: '手动输入',
import: '文件上传',
authKeys: '公钥管理',
authKeysHelper: '是否保存当前公钥信息?',
pubkey: '密钥信息',
pubKeyHelper: '当前密钥信息仅对用户 {0} 生效',
encryptionMode: '加密方式',
@@ -0,0 +1,62 @@
<template>
<div>
<DrawerPro v-model="open" :header="$t('ssh.authKeys')" size="large">
<div v-loading="loading">
<CodemirrorPro
:heightDiff="160"
v-model="conf"
:lineWrapping="true"
placeholder="# The authorized_keys file does not exist or is empty (~/ssh/authorized_keys)"
/>
</div>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" type="primary" @click="onSaveFile">
{{ $t('commons.button.save') }}
</el-button>
</span>
</template>
</DrawerPro>
</div>
</template>
<script setup lang="ts">
import { loadSSHFile, updateSSHByFile } from '@/api/modules/host';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { ref } from 'vue';
const conf = ref();
const loading = ref();
const open = ref();
const acceptParams = async (): Promise<void> => {
loadFile();
open.value = true;
};
const loadFile = async () => {
const res = await loadSSHFile('authKeys');
conf.value = res.data || '';
};
const onSaveFile = async () => {
ElMessageBox.confirm(i18n.global.t('ssh.authKeysHelper'), i18n.global.t('ssh.authKeys'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await updateSSHByFile('authKeys', conf.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
open.value = false;
})
.catch(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>
+15 -6
View File
@@ -38,13 +38,16 @@
</div>
<LayoutContent>
<template #main>
<template #leftToolBar>
<el-radio-group v-model="confShowType" @change="changeMode">
<el-radio-button value="base">{{ $t('database.baseConf') }}</el-radio-button>
<el-radio-button value="all">{{ $t('database.allConf') }}</el-radio-button>
</el-radio-group>
<el-button @click="onOpenDrawer" class="mt-2 ml-2">{{ $t('ssh.pubkey') }}</el-button>
<el-button @click="onOpenDrawer">{{ $t('ssh.pubkey') }}</el-button>
<el-button @click="onOpenAuthKeys">{{ $t('ssh.authKeys') }}</el-button>
</template>
<template #main>
<el-row class="mt-10" v-if="confShowType === 'base'">
<el-col :xs="24" :sm="20" :md="20" :lg="10" :xl="10">
<el-form :model="form" label-position="right" ref="formRef" label-width="100px">
@@ -111,7 +114,7 @@
<div v-if="confShowType === 'all'">
<CodemirrorPro
:heightDiff="450"
:heightDiff="320"
:minHeight="350"
class="mt-5"
v-model="sshConf"
@@ -124,6 +127,7 @@
</template>
</LayoutContent>
<AuthKeys ref="authKeyRef" />
<Cert ref="pubKeyRef" @search="search" />
<Port ref="portRef" @search="search" />
<Address ref="addressRef" @search="search" />
@@ -134,13 +138,14 @@
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import FireRouter from '@/views/host/ssh/index.vue';
import AuthKeys from '@/views/host/ssh/ssh/auth-keys/index.vue';
import Cert from '@/views/host/ssh/ssh/certification/index.vue';
import Root from '@/views/host/ssh/ssh/root/index.vue';
import Port from '@/views/host/ssh/ssh/port/index.vue';
import Address from '@/views/host/ssh/ssh/address/index.vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { getSSHConf, getSSHInfo, operateSSH, updateSSH, updateSSHByfile } from '@/api/modules/host';
import { loadSSHFile, getSSHInfo, operateSSH, updateSSH, updateSSHByFile } from '@/api/modules/host';
import { ElMessageBox, FormInstance } from 'element-plus';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
@@ -151,6 +156,7 @@ const pubKeyRef = ref();
const portRef = ref();
const addressRef = ref();
const rootsRef = ref();
const authKeyRef = ref();
const autoStart = ref('enable');
@@ -178,7 +184,7 @@ const onSaveFile = async () => {
type: 'info',
}).then(async () => {
loading.value = true;
await updateSSHByfile(sshConf.value)
await updateSSHByFile('sshdConf', sshConf.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
@@ -192,6 +198,9 @@ const onSaveFile = async () => {
const onOpenDrawer = () => {
pubKeyRef.value.acceptParams(form.currentUser);
};
const onOpenAuthKeys = () => {
authKeyRef.value.acceptParams();
};
const onChangePort = () => {
portRef.value.acceptParams({ port: form.port });
@@ -285,7 +294,7 @@ const changeI18n = (value: string) => {
};
const loadSSHConf = async () => {
const res = await getSSHConf();
const res = await loadSSHFile('sshdConf');
sshConf.value = res.data || '';
};