🐛 SSH 配置未启用还可以连接.

This commit is contained in:
lijiahang
2024-03-06 17:58:32 +08:00
parent ba338c15de
commit 201956855f
29 changed files with 177 additions and 57 deletions

View File

@@ -9,7 +9,7 @@
* [常见问题](quickstart/faq.md)
* 操作手册
* [资产管理](operator/asset.md)
* [主机运维](operator/host_ops.md)
* [运维审计](operator/host_audit.md)
* [主机运维](operator/host-ops.md)
* [运维审计](operator/asset-audit.md)
* [用户管理](operator/user.md)
* [系统管理](operator/system.md)

View File

@@ -1,5 +1,14 @@
> 版本号严格遵循 Semver 规范。
## v1.0.2
`2024-03-` `release`
🐞 修复 SSH 配置未启用还可以连接
🐞 修复 主机配置保存后无法修改状态
[如何升级](/about/update.md?id=_v102)
## v1.0.1
`2024-03-06` `release`
@@ -11,7 +20,7 @@
🌈 新增 主机连接日志删除/清理
🌈 新增 用户操作日志日志删除/清理
🌈 新增 用户操作日志日志删除/清理
🔨 优化 用户锁定次数/时间可配置
🔨 优化 用户锁定次数/时间可配置
[如何升级](/about/update.md?id=_v101)

View File

@@ -75,8 +75,6 @@ public interface ErrorMessage {
String SESSION_ABSENT = "会话不存在";
String CONNECT_ERROR = "连接失败";
String PATH_NOT_NORMALIZE = "路径不合法";
String OPERATE_ERROR = "操作失败";

View File

@@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@@ -40,8 +41,8 @@ public class AssetAuthorizedDataServiceController {
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/current-host")
@Operation(summary = "查询当前用户已授权的主机")
public AuthorizedHostWrapperVO getCurrentAuthorizedHostGroup() {
return assetAuthorizedDataService.getUserAuthorizedHostGroup(SecurityUtils.getLoginUserId());
public AuthorizedHostWrapperVO getCurrentAuthorizedHost(@RequestParam("type") String type) {
return assetAuthorizedDataService.getUserAuthorizedHost(SecurityUtils.getLoginUserId(), type);
}
@IgnoreLog(IgnoreLogMode.RET)

View File

@@ -43,7 +43,7 @@ public enum HostConfigTypeEnum implements GenericsDataDefinition {
return null;
}
for (HostConfigTypeEnum value : values()) {
if (value.type.equals(type)) {
if (value.type.equalsIgnoreCase(type)) {
return value;
}
}

View File

@@ -26,7 +26,7 @@ public enum HostConnectTypeEnum {
return null;
}
for (HostConnectTypeEnum value : values()) {
if (value.name().equals(type)) {
if (value.name().equalsIgnoreCase(type)) {
return value;
}
}

View File

@@ -9,14 +9,18 @@ package com.orion.ops.module.asset.handler.host.terminal.constant;
*/
public interface TerminalMessage {
String CLOSED_CONNECTION = "closed connection...";
String CONFIG_DISABLED = "SSH configuration has been disabled.";
String AUTHENTICATION_FAILURE = "authentication failure...";
String AUTHENTICATION_FAILURE = "authentication failed. please check the configuration.";
String UNREACHABLE = "remote server unreachable...";
String SERVER_UNREACHABLE = "remote server unreachable. please check the configuration.";
String CONNECTION_TIMEOUT = "connection timeout...";
String CONNECTION_FAILED = "connection failed.";
String FORCED_OFFLINE = "forced offline...";
String CONNECTION_TIMEOUT = "connection timeout.";
String CONNECTION_CLOSED = "connection closed.";
String FORCED_OFFLINE = "forced offline.";
}

View File

@@ -1,5 +1,6 @@
package com.orion.ops.module.asset.handler.host.terminal.handler;
import com.orion.lang.exception.DisabledException;
import com.orion.lang.exception.argument.InvalidArgumentException;
import com.orion.lang.utils.Exceptions;
import com.orion.lang.utils.collect.Maps;
@@ -17,6 +18,7 @@ import com.orion.ops.module.asset.entity.dto.HostTerminalConnectDTO;
import com.orion.ops.module.asset.entity.request.host.HostConnectLogCreateRequest;
import com.orion.ops.module.asset.enums.HostConnectStatusEnum;
import com.orion.ops.module.asset.enums.HostConnectTypeEnum;
import com.orion.ops.module.asset.handler.host.terminal.constant.TerminalMessage;
import com.orion.ops.module.asset.handler.host.terminal.enums.OutputTypeEnum;
import com.orion.ops.module.asset.handler.host.terminal.model.request.TerminalCheckRequest;
import com.orion.ops.module.asset.handler.host.terminal.model.response.TerminalCheckResponse;
@@ -82,9 +84,12 @@ public class TerminalCheckHandler extends AbstractTerminalHandler<TerminalCheckR
log.info("TerminalCheckHandler-handle success userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId);
} catch (InvalidArgumentException e) {
ex = e;
log.error("TerminalCheckHandler-handle error userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId, e);
log.error("TerminalCheckHandler-handle start error userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId, e);
} catch (DisabledException e) {
ex = Exceptions.runtime(TerminalMessage.CONFIG_DISABLED);
log.error("TerminalCheckHandler-handle disabled error userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId);
} catch (Exception e) {
ex = Exceptions.runtime(ErrorMessage.CONNECT_ERROR);
ex = Exceptions.runtime(TerminalMessage.CONNECTION_FAILED);
log.error("TerminalCheckHandler-handle exception userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId, e);
}
// 记录主机日志

View File

@@ -1,7 +1,6 @@
package com.orion.ops.module.asset.handler.host.terminal.handler;
import com.orion.lang.exception.AuthenticationException;
import com.orion.lang.exception.ConnectionRuntimeException;
import com.orion.lang.exception.TimeoutException;
import com.orion.lang.exception.argument.InvalidArgumentException;
import com.orion.lang.utils.Exceptions;
@@ -151,9 +150,6 @@ public class TerminalConnectHandler extends AbstractTerminalHandler<TerminalConn
if (Exceptions.isCausedBy(e, TimeoutException.class)) {
// 连接超时
return TerminalMessage.CONNECTION_TIMEOUT;
} else if (Exceptions.isCausedBy(e, ConnectionRuntimeException.class)) {
// 无法连接
return TerminalMessage.UNREACHABLE;
} else if (Exceptions.isCausedBy(e, AuthenticationException.class)) {
// 认证失败
return TerminalMessage.AUTHENTICATION_FAILURE;
@@ -162,7 +158,7 @@ public class TerminalConnectHandler extends AbstractTerminalHandler<TerminalConn
return e.getMessage();
} else {
// 其他错误
return TerminalMessage.UNREACHABLE;
return TerminalMessage.SERVER_UNREACHABLE;
}
}

View File

@@ -56,7 +56,7 @@ public abstract class TerminalSession implements ITerminalSession {
.type(OutputTypeEnum.CLOSE.getType())
.sessionId(this.sessionId)
.forceClose(BooleanBit.of(this.forceOffline).getValue())
.msg(this.forceOffline ? TerminalMessage.FORCED_OFFLINE : TerminalMessage.CLOSED_CONNECTION)
.msg(this.forceOffline ? TerminalMessage.FORCED_OFFLINE : TerminalMessage.CONNECTION_CLOSED)
.build();
WebSockets.sendText(channel, OutputTypeEnum.CLOSE.format(resp));
}

View File

@@ -30,9 +30,10 @@ public interface AssetAuthorizedDataService {
* 查询用户已授权的主机主机
*
* @param userId userId
* @param type type
* @return group
*/
AuthorizedHostWrapperVO getUserAuthorizedHostGroup(Long userId);
AuthorizedHostWrapperVO getUserAuthorizedHost(Long userId, String type);
/**
* 获取用户已授权的主机id 不查询角色

View File

@@ -66,4 +66,13 @@ public interface HostConfigService {
*/
void initHostConfig(Long hostId);
/**
* 获取启用配置的 hostId
*
* @param type type
* @param hostIdList hostIdList
* @return hostId
*/
List<Long> getEnabledConfigHostId(String type, List<Long> hostIdList);
}

View File

@@ -55,6 +55,9 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
@Resource
private HostService hostService;
@Resource
private HostConfigService hostConfigService;
@Resource
private HostKeyService hostKeyService;
@@ -88,10 +91,10 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
}
@Override
public AuthorizedHostWrapperVO getUserAuthorizedHostGroup(Long userId) {
public AuthorizedHostWrapperVO getUserAuthorizedHost(Long userId, String type) {
if (systemUserApi.isAdminUser(userId)) {
// 管理员查询所有
return this.buildUserAuthorizedHostGroup(userId, null);
return this.buildUserAuthorizedHost(userId, null, type);
} else {
// 其他用户 查询授权的数据
List<Long> authorizedIdList = dataPermissionApi.getUserAuthorizedRelIdList(DataPermissionTypeEnum.HOST_GROUP, userId);
@@ -102,7 +105,7 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
.hostList(Lists.empty())
.build();
}
return this.buildUserAuthorizedHostGroup(userId, authorizedIdList);
return this.buildUserAuthorizedHost(userId, authorizedIdList, type);
}
}
@@ -173,24 +176,27 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
*
* @param userId userId
* @param authorizedGroupIdList authorizedGroupIdList
* @param type type
* @return tree
*/
@SneakyThrows
private AuthorizedHostWrapperVO buildUserAuthorizedHostGroup(Long userId, List<Long> authorizedGroupIdList) {
private AuthorizedHostWrapperVO buildUserAuthorizedHost(Long userId, List<Long> authorizedGroupIdList, String type) {
final boolean allData = Lists.isEmpty(authorizedGroupIdList);
AuthorizedHostWrapperVO wrapper = new AuthorizedHostWrapperVO();
// 查询我的收藏
Future<List<Long>> favoriteResult = favoriteApi.getFavoriteRelIdListAsync(FavoriteTypeEnum.HOST, userId);
// 查询最近连接的主机
Future<List<Long>> latestConnectHostIdList = hostConnectLogService.getLatestConnectHostIdAsync(HostConnectTypeEnum.SSH, userId);
// 查询别名
Future<Map<Long, String>> dataAliasResult = dataExtraApi.getExtraItemValuesByCacheAsync(userId, DataExtraTypeEnum.HOST, DataExtraItems.ALIAS);
// 查询颜色
Future<Map<Long, String>> dataColorResult = dataExtraApi.getExtraItemValuesByCacheAsync(userId, DataExtraTypeEnum.HOST, DataExtraItems.COLOR);
Future<List<Long>> latestConnectHostIdList = hostConnectLogService.getLatestConnectHostIdAsync(HostConnectTypeEnum.of(type), userId);
// 查询主机拓展信息
Future<List<Map<Long, String>>> hostExtraResult = dataExtraApi.getExtraItemsValuesByCacheAsync(userId,
DataExtraTypeEnum.HOST,
Lists.of(DataExtraItems.ALIAS, DataExtraItems.COLOR));
// 查询分组
List<DataGroupDTO> dataGroup = dataGroupApi.getDataGroupList(DataGroupTypeEnum.HOST);
// 查询分组引用
Map<Long, Set<Long>> dataGroupRel = dataGroupRelApi.getGroupRelList(DataGroupTypeEnum.HOST);
// 查询配置启用的主机
List<Long> enabledConfigHostId = this.getEnabledConfigHostId(allData, dataGroupRel, type);
// 过滤已经授权的分组
if (!allData) {
// 构建已授权的分组
@@ -209,17 +215,47 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
wrapper.setHostList(this.getAuthorizedHostList(allData,
dataGroup,
dataGroupRel,
authorizedGroupIdList));
authorizedGroupIdList,
enabledConfigHostId));
// 设置主机拓展信息
this.getAuthorizedHostExtra(wrapper.getHostList(),
favoriteResult.get(),
dataAliasResult.get(),
dataColorResult.get());
hostExtraResult.get());
// 设置最近连接的主机
wrapper.setLatestHosts(new LinkedHashSet<>(latestConnectHostIdList.get()));
return wrapper;
}
/**
* 获取已启用配置的 hostId
*
* @param allData allData
* @param dataGroupRel dataGroupRel
* @param type type
* @return enabledHostIdList
*/
private List<Long> getEnabledConfigHostId(boolean allData,
Map<Long, Set<Long>> dataGroupRel,
String type) {
List<Long> hostIdList = null;
if (!allData) {
// 非全部数据从分组映射中获取
hostIdList = dataGroupRel.values()
.stream()
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toList());
if (hostIdList.isEmpty()) {
return Lists.empty();
}
}
// 查询启用配置的主机
List<Long> enabledConfigHostId = hostConfigService.getEnabledConfigHostId(type, hostIdList);
// 从分组引用中移除
dataGroupRel.forEach((k, v) -> v.removeIf(s -> !enabledConfigHostId.contains(s)));
return enabledConfigHostId;
}
/**
* 构建主机分组树
*
@@ -265,14 +301,19 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
* @param dataGroup dataGroup
* @param dataGroupRel dataGroupRel
* @param authorizedGroupIdList authorizedGroupIdList
* @param enabledConfigHostId enabledConfigHostId
* @return hosts
*/
private List<HostVO> getAuthorizedHostList(boolean allData,
List<DataGroupDTO> dataGroup,
Map<Long, Set<Long>> dataGroupRel,
List<Long> authorizedGroupIdList) {
List<Long> authorizedGroupIdList,
List<Long> enabledConfigHostId) {
// 查询主机列表
List<HostVO> hosts = hostService.getHostListByCache();
List<HostVO> hosts = hostService.getHostListByCache()
.stream()
.filter(s -> enabledConfigHostId.contains(s.getId()))
.collect(Collectors.toList());
// 全部数据直接返回
if (allData) {
return hosts;
@@ -296,15 +337,13 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
/**
* 设置授权主机的额外参数
*
* @param hosts hosts
* @param favorite favorite
* @param aliasMap aliasMap
* @param colorMap colorMap
* @param hosts hosts
* @param favorite favorite
* @param extraList extraList
*/
private void getAuthorizedHostExtra(List<HostVO> hosts,
List<Long> favorite,
Map<Long, String> aliasMap,
Map<Long, String> colorMap) {
List<Map<Long, String>> extraList) {
if (Lists.isEmpty(hosts)) {
return;
}
@@ -321,6 +360,7 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
hosts.get(i).setTags(tags.get(i));
}
// 设置主机别名
Map<Long, String> aliasMap = extraList.get(0);
if (!Maps.isEmpty(aliasMap)) {
hosts.forEach(s -> {
String alias = aliasMap.get(s.getId());
@@ -330,6 +370,7 @@ public class AssetAuthorizedDataServiceImpl implements AssetAuthorizedDataServic
});
}
// 设置主机颜色
Map<Long, String> colorMap = extraList.get(1);
if (!Maps.isEmpty(colorMap)) {
hosts.forEach(s -> {
HostColorExtraModel color = JSON.parseObject(colorMap.get(s.getId()), HostColorExtraModel.class);

View File

@@ -1,8 +1,10 @@
package com.orion.ops.module.asset.service.impl;
import com.orion.lang.utils.Exceptions;
import com.orion.ops.framework.biz.operator.log.core.utils.OperatorLogs;
import com.orion.ops.framework.common.constant.Const;
import com.orion.ops.framework.common.constant.ErrorMessage;
import com.orion.ops.framework.common.enums.BooleanBit;
import com.orion.ops.framework.common.enums.EnableStatus;
import com.orion.ops.framework.common.handler.data.model.GenericsDataModel;
import com.orion.ops.framework.common.handler.data.strategy.MapDataStrategy;
@@ -21,10 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -66,6 +65,10 @@ public class HostConfigServiceImpl implements HostConfigService {
if (config == null) {
return null;
}
// 检查配置状态
if (!BooleanBit.toBoolean(config.getStatus())) {
throw Exceptions.disabled();
}
return type.parse(config.getConfig());
}
@@ -145,6 +148,7 @@ public class HostConfigServiceImpl implements HostConfigService {
update.setId(config.getId());
update.setStatus(status);
update.setVersion(version);
update.setUpdateTime(new Date());
int effect = hostConfigDAO.updateById(update);
Valid.version(effect);
return update.getVersion();
@@ -167,6 +171,20 @@ public class HostConfigServiceImpl implements HostConfigService {
hostConfigDAO.insertBatch(configs);
}
@Override
public List<Long> getEnabledConfigHostId(String type, List<Long> hostIdList) {
return hostConfigDAO.of()
.createValidateWrapper()
.select(HostConfigDO::getHostId)
.eq(HostConfigDO::getType, type)
.eq(HostConfigDO::getStatus, BooleanBit.TRUE.getValue())
.in(HostConfigDO::getHostId, hostIdList)
.then()
.stream()
.map(HostConfigDO::getHostId)
.collect(Collectors.toList());
}
/**
* 通过类型获取配置
*

View File

@@ -101,6 +101,16 @@ public interface DataExtraApi {
*/
Future<Map<Long, String>> getExtraItemValuesByCacheAsync(Long userId, DataExtraTypeEnum type, String item);
/**
* 异步查询额外配置项 (查询缓存)
*
* @param userId userId
* @param type type
* @param items items
* @return value
*/
Future<List<Map<Long, String>>> getExtraItemsValuesByCacheAsync(Long userId, DataExtraTypeEnum type, List<String> items);
/**
* 查询额外配置
*

View File

@@ -102,6 +102,13 @@ public class DataExtraApiImpl implements DataExtraApi {
return CompletableFuture.completedFuture(this.getExtraItemValuesByCache(userId, type, item));
}
@Override
public Future<List<Map<Long, String>>> getExtraItemsValuesByCacheAsync(Long userId, DataExtraTypeEnum type, List<String> items) {
Valid.allNotNull(userId, type);
Valid.notEmpty(items);
return CompletableFuture.completedFuture(dataExtraService.getExtraItemsValuesByCache(userId, type.name(), items));
}
@Override
public DataExtraDTO getExtraItem(DataExtraQueryDTO dto, DataExtraTypeEnum type) {
Valid.allNotNull(dto.getUserId(), dto.getRelId(), dto.getItem());

View File

@@ -85,6 +85,16 @@ public interface DataExtraService {
*/
Map<Long, String> getExtraItemValuesByCache(Long userId, String type, String item);
/**
* 查询额外配置项 (查询缓存)
*
* @param userId userId
* @param type type
* @param items items
* @return [relId:value, relId:value]
*/
List<Map<Long, String>> getExtraItemsValuesByCache(Long userId, String type, List<String> items);
/**
* 查询额外配置
*

View File

@@ -170,6 +170,13 @@ public class DataExtraServiceImpl implements DataExtraService {
return Maps.map(entities, Long::valueOf, Function.identity());
}
@Override
public List<Map<Long, String>> getExtraItemsValuesByCache(Long userId, String type, List<String> items) {
return items.stream()
.map(s -> this.getExtraItemValuesByCache(userId, type, s))
.collect(Collectors.toList());
}
@Override
public DataExtraDO getExtraItem(DataExtraQueryRequest request) {
return dataExtraDAO.of()

View File

@@ -17,8 +17,8 @@ export interface AuthorizedHostQueryResponse {
/**
* 查询当前用户已授权的主机
*/
export function getCurrentAuthorizedHost() {
return axios.get<AuthorizedHostQueryResponse>('/asset/authorized-data/current-host');
export function getCurrentAuthorizedHost(type: string) {
return axios.get<AuthorizedHostQueryResponse>('/asset/authorized-data/current-host', { params: { type } });
}
/**

View File

@@ -128,7 +128,7 @@ export default defineStore('terminal', {
if (this.hosts.hostList?.length) {
return;
}
const { data } = await getCurrentAuthorizedHost();
const { data } = await getCurrentAuthorizedHost('ssh');
Object.keys(data).forEach(k => {
this.hosts[k as keyof AuthorizedHostQueryResponse] = data[k as keyof AuthorizedHostQueryResponse] as any;
});

View File

@@ -45,7 +45,9 @@
unchecked-text="使用原密码" />
</a-form-item>
<!-- 秘钥id -->
<a-form-item field="keyId" label="主机秘钥">
<a-form-item field="keyId"
label="主机秘钥"
extra="密码和秘钥二选一 优先使用秘钥">
<host-key-selector v-model="formModel.keyId" />
</a-form-item>
</a-form>

View File

@@ -260,7 +260,7 @@
setLoading(false);
Message.success('修改成功');
// 回调 props
emits('submitted', { ...props.content, config: { ...formModel.value } });
emits('submitted', { ...props.content, ...config, config: { ...formModel.value } });
} catch (e) {
} finally {
setLoading(false);

View File

@@ -17,7 +17,7 @@
<!-- 主机列表 -->
<host-list-view class="host-list"
:hostList="hostList"
empty-value="当前分组内无授权主机!" />
empty-value="当前分组内无授权主机/主机未启用 SSH 配置!" />
</div>
</template>

View File

@@ -10,7 +10,7 @@
<!-- 列表视图 -->
<host-list-view v-if="NewConnectionType.LIST === newConnectionType"
:hostList="hostList"
empty-value="无授权主机!" />
empty-value="无授权主机/主机未启用 SSH 配置!" />
<!-- 我的收藏 -->
<host-list-view v-if="NewConnectionType.FAVORITE === newConnectionType"
class="list-view-container"

View File

@@ -46,7 +46,7 @@
</div>
<!-- 已关闭-右侧操作 -->
<div v-if="isClose" class="sftp-table-header-right">
<span class="close-message">{{ closeMessage }}</span>
<span class="close-message" :title="closeMessage">{{ closeMessage }}</span>
</div>
<!-- 路径编辑模式-右侧操作 -->
<a-space v-else-if="pathEditable" class="sftp-table-header-right">

View File

@@ -190,10 +190,10 @@
// 关闭回调
const onClose = (forceClose: string, msg: string) => {
console.log(forceClose);
console.log(msg);
closed.value = true;
closeMessage.value = msg;
setTableLoading(false);
setEditorLoading(false);
};
// 接收列表回调

View File

@@ -47,6 +47,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
});
} else {
// 未成功提示错误信息
session.resolver?.onClose('0', msg);
Message.error(msg || '建立 SFTP 失败');
}
}
@@ -75,6 +76,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
session.connect();
} else {
// 未成功提示错误信息
session.resolver?.onClose('0', msg);
Message.error(msg || '打开 SFTP 失败');
}
}