diff --git a/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageConfig.java b/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageConfig.java new file mode 100644 index 00000000000..c111b3d2fda --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageConfig.java @@ -0,0 +1,21 @@ +package org.zstack.header.storage.addon.primary; + +/** + * Interface for external primary storage config desensitization. + * + * Each external primary storage plugin should implement this on its Config class. + * ZStack will call {@link #desensitize()} before outputting config to API responses or logs, + * so plugin developers only need to implement this method to mask sensitive fields. + */ +public interface ExternalPrimaryStorageConfig { + /** + * The identity of the external primary storage plugin (e.g. "zbs"). + */ + String getIdentity(); + + /** + * Return a desensitized copy of this config with sensitive fields masked. + * The original object must not be modified. + */ + ExternalPrimaryStorageConfig desensitize(); +} diff --git a/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageInventory.java b/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageInventory.java index a15ed211307..ab4a4c49009 100644 --- a/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageInventory.java +++ b/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageInventory.java @@ -2,16 +2,36 @@ import org.zstack.header.search.Inventory; import org.zstack.header.storage.primary.PrimaryStorageInventory; +import org.zstack.utils.BeanUtils; +import org.zstack.utils.Utils; import org.zstack.utils.gson.JSONObjectUtil; +import org.zstack.utils.logging.CLogger; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @Inventory(mappingVOClass = ExternalPrimaryStorageVO.class) public class ExternalPrimaryStorageInventory extends PrimaryStorageInventory { + private static final CLogger logger = Utils.getLogger(ExternalPrimaryStorageInventory.class); + private static final Map> configClassRegistry = new ConcurrentHashMap<>(); + + static { + for (Class clz : BeanUtils.reflections.getSubTypesOf(ExternalPrimaryStorageConfig.class)) { + if (clz.isInterface()) { + continue; + } + try { + ExternalPrimaryStorageConfig instance = clz.newInstance(); + configClassRegistry.put(instance.getIdentity(), clz); + } catch (Exception e) { + logger.warn(String.format("failed to register ExternalPrimaryStorageConfig: %s", clz.getName()), e); + } + } + } + private String identity; /** @@ -60,44 +80,22 @@ public ExternalPrimaryStorageInventory() { public ExternalPrimaryStorageInventory(ExternalPrimaryStorageVO lvo) { super(lvo); identity = lvo.getIdentity(); - config = JSONObjectUtil.toObject(lvo.getConfig(), LinkedHashMap.class); - desensitizeConfig(config); addonInfo = JSONObjectUtil.toObject(lvo.getAddonInfo(), LinkedHashMap.class); outputProtocols = lvo.getOutputProtocols().stream().map(PrimaryStorageOutputProtocolRefVO::getOutputProtocol).collect(Collectors.toList()); defaultProtocol = lvo.getDefaultProtocol(); - } - - public static ExternalPrimaryStorageInventory valueOf(ExternalPrimaryStorageVO lvo) { - return new ExternalPrimaryStorageInventory(lvo); - } - private static void desensitizeConfig(Map config) { - if (config == null) return; - desensitizeUrlList(config, "mdsUrls"); - desensitizeUrlList(config, "mdsInfos"); - } - - private static void desensitizeUrlList(Map config, String key) { - Object urls = config.get(key); - if (urls instanceof List) { - List desensitized = new ArrayList<>(); - for (Object url : (List) urls) { - desensitized.add(desensitizeUrl(String.valueOf(url))); - } - config.put(key, desensitized); + Class configClass = configClassRegistry.get(identity); + if (configClass != null) { + ExternalPrimaryStorageConfig typedConfig = JSONObjectUtil.toObject(lvo.getConfig(), configClass); + ExternalPrimaryStorageConfig desensitized = typedConfig.desensitize(); + config = JSONObjectUtil.toObject(JSONObjectUtil.toJsonString(desensitized), LinkedHashMap.class); + } else { + config = JSONObjectUtil.toObject(lvo.getConfig(), LinkedHashMap.class); } } - private static String desensitizeUrl(String url) { - int atIndex = url.lastIndexOf('@'); - if (atIndex > 0) { - int schemeIndex = url.indexOf("://"); - if (schemeIndex >= 0 && schemeIndex < atIndex) { - return url.substring(0, schemeIndex + 3) + "***" + url.substring(atIndex); - } - return "***" + url.substring(atIndex); - } - return url; + public static ExternalPrimaryStorageInventory valueOf(ExternalPrimaryStorageVO lvo) { + return new ExternalPrimaryStorageInventory(lvo); } public String getIdentity() { diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/Config.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/Config.java index ca7d1c24066..68070bfef2b 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/Config.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/Config.java @@ -1,15 +1,18 @@ package org.zstack.storage.zbs; +import org.zstack.header.storage.addon.primary.ExternalPrimaryStorageConfig; +import org.zstack.utils.gson.JSONObjectUtil; import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** * @author Xingwei Yu * @date 2024/4/2 11:13 */ -public class Config { +public class Config implements ExternalPrimaryStorageConfig { public static class Pool { public String logicalName; public String aliasName; @@ -22,11 +25,29 @@ public Pool(String logicalName, String aliasName) { public Pool() {} } + private static final Pattern URI_CREDENTIAL_PATTERN = Pattern.compile(":[^:@]*@"); + private List mdsUrls; + + @Override + public String getIdentity() { + return ZbsConstants.IDENTITY; + } private List pools; private String logicalPoolName; private transient List poolNames; + @Override + public ExternalPrimaryStorageConfig desensitize() { + Config copy = JSONObjectUtil.toObject(JSONObjectUtil.toJsonString(this), Config.class); + if (copy.mdsUrls != null) { + copy.mdsUrls = copy.mdsUrls.stream() + .map(url -> URI_CREDENTIAL_PATTERN.matcher(url).replaceFirst(":*****@")) + .collect(Collectors.toList()); + } + return copy; + } + public List getMdsUrls() { return mdsUrls; } diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsInfo.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsInfo.java index 4085e6657af..da1cb42b4fa 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsInfo.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsInfo.java @@ -1,5 +1,8 @@ package org.zstack.storage.zbs; +import org.zstack.header.log.NoLogging; + +import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -9,9 +12,10 @@ * @author Xingwei Yu * @date 2024/4/10 23:18 */ -public class MdsInfo { +public class MdsInfo implements Serializable { private String username; - private String password; + @NoLogging + private transient String password; private int port = 22; private String addr; private String externalAddr; diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy index f07add523d3..1b75e2ed200 100644 --- a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy @@ -532,9 +532,9 @@ class ZbsPrimaryStorageCase extends SubCase { def addonInfo = Q.New(ExternalPrimaryStorageVO.class).select(ExternalPrimaryStorageVO_.addonInfo).eq(ExternalPrimaryStorageVO_.uuid, ps.uuid).findValue() assert addonInfo == "{\"clusterInfo\":{\"uuid\":\"123456789\",\"version\":\"1.6.1-for-test\"}," + - "\"mdsInfos\":[{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," + - "{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," + - "{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}]," + + "\"mdsInfos\":[{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," + + "{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," + + "{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}]," + "\"logicalPoolInfos\":[{\"physicalPoolID\":1,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":1,\"usedSize\":322961408,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":968884224,\"physicalPoolName\":\"pool1\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool1\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":3221225472}," + "{\"physicalPoolID\":2,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":2,\"usedSize\":123456789,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":123456789,\"physicalPoolName\":\"pool2\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool2\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":987654321}]}" assert data == null @@ -566,9 +566,9 @@ class ZbsPrimaryStorageCase extends SubCase { addonInfo = Q.New(ExternalPrimaryStorageVO.class).select(ExternalPrimaryStorageVO_.addonInfo).eq(ExternalPrimaryStorageVO_.uuid, ps.uuid).findValue() assert addonInfo == "{\"clusterInfo\":{\"uuid\":\"123456789\",\"version\":\"1.6.1-for-test\"}," + - "\"mdsInfos\":[{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," + - "{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," + - "{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}]," + + "\"mdsInfos\":[{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," + + "{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}," + + "{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Connected\"}]," + "\"logicalPoolInfos\":[{\"physicalPoolID\":1,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":1,\"usedSize\":322961408,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":968884224,\"physicalPoolName\":\"pool1\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool1\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":3221225472}," + "{\"physicalPoolID\":2,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":2,\"usedSize\":123456789,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":123456789,\"physicalPoolName\":\"pool2\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool2\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":987654321}]}" @@ -615,9 +615,9 @@ class ZbsPrimaryStorageCase extends SubCase { addonInfo = Q.New(ExternalPrimaryStorageVO.class).select(ExternalPrimaryStorageVO_.addonInfo).eq(ExternalPrimaryStorageVO_.uuid, ps.uuid).findValue() assert addonInfo == "{\"clusterInfo\":{\"uuid\":\"123456789\",\"version\":\"1.6.1-for-test\"}," + - "\"mdsInfos\":[{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," + - "{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," + - "{\"username\":\"root\",\"password\":\"password\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}]," + + "\"mdsInfos\":[{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.1\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," + + "{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.2\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}," + + "{\"username\":\"root\",\"port\":22,\"addr\":\"127.0.1.3\",\"externalAddr\":\"127.0.0.1\",\"status\":\"Disconnected\"}]," + "\"logicalPoolInfos\":[{\"physicalPoolID\":1,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":1,\"usedSize\":322961408,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":968884224,\"physicalPoolName\":\"pool1\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool1\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":3221225472}," + "{\"physicalPoolID\":2,\"redundanceAndPlaceMentPolicy\":{\"copysetNum\":300,\"replicaNum\":3,\"zoneNum\":3},\"logicalPoolID\":2,\"usedSize\":123456789,\"quota\":0,\"createTime\":1735875794,\"type\":0,\"rawWalUsedSize\":0,\"allocateStatus\":0,\"rawUsedSize\":123456789,\"physicalPoolName\":\"pool2\",\"capacity\":579933831168,\"logicalPoolName\":\"lpool2\",\"userPolicy\":\"eyJwb2xpY3kiIDogMX0=\",\"allocatedSize\":987654321}]}"