From 58ee269855c17a2373c1c2ffc355338b9b4051fa Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 28 Apr 2026 09:42:29 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(jvm):=20=E6=94=B6=E7=B4=A7?= =?UTF-8?q?=20JMX=20domain=20allowlist=20=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 helper runtime 中对直接 ObjectName、资源浏览、变更预览和监控路径统一执行 domain allowlist,阻断默认域别名和空白后缀绕过。 --- internal/jvm/jmx_provider_test.go | 300 ++++++++++++++++++ .../jmxhelper_assets/jmx-helper-runtime.jar | Bin 26995 -> 27512 bytes .../src/com/gonavi/fixture/CacheSettings.java | 22 ++ .../gonavi/fixture/CacheSettingsMBean.java | 6 + .../src/com/gonavi/fixture/JMXTestServer.java | 8 + .../src/com/gonavi/jmxhelper/JmxRuntime.java | 75 ++++- 6 files changed, 401 insertions(+), 10 deletions(-) diff --git a/internal/jvm/jmx_provider_test.go b/internal/jvm/jmx_provider_test.go index 45ca682..3a183aa 100644 --- a/internal/jvm/jmx_provider_test.go +++ b/internal/jvm/jmx_provider_test.go @@ -287,6 +287,10 @@ func TestJMXProviderRealJMXRoundTrip(t *testing.T) { } provider := NewJMXProvider() + monitoringProvider, ok := provider.(MonitoringCapableProvider) + if !ok { + t.Fatal("expected JMX provider to implement monitoring snapshots") + } fixture := startJMXFixture(t) readOnly := false cfg := connection.ConnectionConfig{ @@ -335,14 +339,310 @@ func TestJMXProviderRealJMXRoundTrip(t *testing.T) { } mbean := mbeans[0] + blockedDomainPath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindDomain, + Domain: "java.lang", + }) + _, err = provider.ListResources(context.Background(), cfg, blockedDomainPath) + if err == nil { + t.Fatal("expected list on blocked domain to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain list context, got %v", err) + } + + _, err = provider.GetValue(context.Background(), cfg, blockedDomainPath) + if err == nil { + t.Fatal("expected get on blocked domain to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain get context, got %v", err) + } + + blockedMBeanPath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindMBean, + ObjectName: "java.lang:type=Memory", + }) + _, err = provider.ListResources(context.Background(), cfg, blockedMBeanPath) + if err == nil { + t.Fatal("expected list on blocked domain mbean to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain mbean list context, got %v", err) + } + + _, err = provider.GetValue(context.Background(), cfg, blockedMBeanPath) + if err == nil { + t.Fatal("expected direct mbean get on blocked domain to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain mbean get context, got %v", err) + } + + blockedAttributePath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindAttribute, + ObjectName: "java.lang:type=Memory", + Attribute: "HeapMemoryUsage", + }) + _, err = provider.GetValue(context.Background(), cfg, blockedAttributePath) + if err == nil { + t.Fatal("expected direct attribute get on blocked domain to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain attribute get context, got %v", err) + } + + blockedOperationPath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindOperation, + ObjectName: "java.lang:type=Memory", + Operation: "gc", + }) + _, err = provider.GetValue(context.Background(), cfg, blockedOperationPath) + if err == nil { + t.Fatal("expected direct operation get on blocked domain to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain operation get context, got %v", err) + } + + _, err = provider.PreviewChange(context.Background(), cfg, ChangeRequest{ + ProviderMode: ModeJMX, + ResourceID: blockedOperationPath, + Action: "invoke", + Reason: "尝试跨域操作预览", + }) + if err == nil { + t.Fatal("expected preview on blocked domain operation to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain operation preview context, got %v", err) + } + + _, err = provider.ApplyChange(context.Background(), cfg, ChangeRequest{ + ProviderMode: ModeJMX, + ResourceID: blockedOperationPath, + Action: "invoke", + Reason: "尝试跨域操作调用", + }) + if err == nil { + t.Fatal("expected apply on blocked domain operation to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain operation apply context, got %v", err) + } + + _, err = provider.PreviewChange(context.Background(), cfg, ChangeRequest{ + ProviderMode: ModeJMX, + ResourceID: blockedAttributePath, + Action: "update", + Reason: "尝试跨域属性预览", + Payload: map[string]any{ + "value": "blocked", + }, + }) + if err == nil { + t.Fatal("expected preview on blocked domain attribute to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain attribute preview context, got %v", err) + } + + _, err = provider.ApplyChange(context.Background(), cfg, ChangeRequest{ + ProviderMode: ModeJMX, + ResourceID: blockedAttributePath, + Action: "update", + Reason: "尝试跨域属性修改", + Payload: map[string]any{ + "value": "blocked", + }, + }) + if err == nil { + t.Fatal("expected apply on blocked domain attribute to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain attribute apply context, got %v", err) + } + + defaultDomainMBeanPath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindMBean, + ObjectName: ":type=CacheSettings,name=DefaultDomainCache", + }) + _, err = provider.ListResources(context.Background(), cfg, defaultDomainMBeanPath) + if err == nil { + t.Fatal("expected list on default domain alias mbean to fail") + } + if got := err.Error(); !containsAll(got, "domain") { + t.Fatalf("expected default domain alias mbean list context, got %v", err) + } + + defaultDomainAttributePath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindAttribute, + ObjectName: ":type=CacheSettings,name=DefaultDomainCache", + Attribute: "Mode", + }) + _, err = provider.GetValue(context.Background(), cfg, defaultDomainAttributePath) + if err == nil { + t.Fatal("expected get on default domain alias attribute to fail") + } + if got := err.Error(); !containsAll(got, "domain") { + t.Fatalf("expected default domain alias attribute get context, got %v", err) + } + + defaultDomainOperationPath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindOperation, + ObjectName: ":type=CacheSettings,name=DefaultDomainCache", + Operation: "resize", + Signature: []string{"int", "boolean"}, + }) + _, err = provider.PreviewChange(context.Background(), cfg, ChangeRequest{ + ProviderMode: ModeJMX, + ResourceID: defaultDomainOperationPath, + Action: "invoke", + Reason: "尝试默认域别名操作预览", + Payload: map[string]any{ + "args": []any{3, true}, + }, + }) + if err == nil { + t.Fatal("expected preview on default domain alias operation to fail") + } + if got := err.Error(); !containsAll(got, "domain") { + t.Fatalf("expected default domain alias operation preview context, got %v", err) + } + + _, err = provider.ApplyChange(context.Background(), cfg, ChangeRequest{ + ProviderMode: ModeJMX, + ResourceID: defaultDomainOperationPath, + Action: "invoke", + Reason: "尝试默认域别名操作调用", + Payload: map[string]any{ + "args": []any{3, true}, + }, + }) + if err == nil { + t.Fatal("expected apply on default domain alias operation to fail") + } + if got := err.Error(); !containsAll(got, "domain") { + t.Fatalf("expected default domain alias operation apply context, got %v", err) + } + + whitespaceDomainMBeanPath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindMBean, + ObjectName: "com.gonavi.fixture :type=CacheSettings,name=WhitespaceDomainCache", + }) + _, err = provider.ListResources(context.Background(), cfg, whitespaceDomainMBeanPath) + if err == nil { + t.Fatal("expected list on whitespace-suffixed domain mbean to fail") + } + if got := err.Error(); !containsAll(got, "domain", "com.gonavi.fixture") { + t.Fatalf("expected whitespace-suffixed domain mbean list context, got %v", err) + } + + whitespaceDomainAttributePath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindAttribute, + ObjectName: "com.gonavi.fixture :type=CacheSettings,name=WhitespaceDomainCache", + Attribute: "Mode", + }) + _, err = provider.GetValue(context.Background(), cfg, whitespaceDomainAttributePath) + if err == nil { + t.Fatal("expected get on whitespace-suffixed domain attribute to fail") + } + if got := err.Error(); !containsAll(got, "domain", "com.gonavi.fixture") { + t.Fatalf("expected whitespace-suffixed domain attribute get context, got %v", err) + } + + whitespaceDomainOperationPath := buildJMXResourcePath(jmxResourceTarget{ + Kind: jmxResourceKindOperation, + ObjectName: "com.gonavi.fixture :type=CacheSettings,name=WhitespaceDomainCache", + Operation: "resize", + Signature: []string{"int", "boolean"}, + }) + _, err = provider.PreviewChange(context.Background(), cfg, ChangeRequest{ + ProviderMode: ModeJMX, + ResourceID: whitespaceDomainOperationPath, + Action: "invoke", + Reason: "尝试空白后缀域操作预览", + Payload: map[string]any{ + "args": []any{4, true}, + }, + }) + if err == nil { + t.Fatal("expected preview on whitespace-suffixed domain operation to fail") + } + if got := err.Error(); !containsAll(got, "domain", "com.gonavi.fixture") { + t.Fatalf("expected whitespace-suffixed domain operation preview context, got %v", err) + } + + _, err = provider.ApplyChange(context.Background(), cfg, ChangeRequest{ + ProviderMode: ModeJMX, + ResourceID: whitespaceDomainOperationPath, + Action: "invoke", + Reason: "尝试空白后缀域操作调用", + Payload: map[string]any{ + "args": []any{4, true}, + }, + }) + if err == nil { + t.Fatal("expected apply on whitespace-suffixed domain operation to fail") + } + if got := err.Error(); !containsAll(got, "domain", "com.gonavi.fixture") { + t.Fatalf("expected whitespace-suffixed domain operation apply context, got %v", err) + } + + _, err = monitoringProvider.GetMonitoringSnapshot(context.Background(), cfg, nil) + if err == nil { + t.Fatal("expected monitor on blocked domain allowlist to fail") + } + if got := err.Error(); !containsAll(got, "domain", "java.lang") { + t.Fatalf("expected blocked domain monitor context, got %v", err) + } + + monitoringCfg := cfg + monitoringCfg.JVM.JMX.DomainAllowlist = []string{"com.gonavi.fixture", "java.lang"} + monitoringSnapshot, err := monitoringProvider.GetMonitoringSnapshot(context.Background(), monitoringCfg, nil) + if err != nil { + t.Fatalf("expected monitor with java.lang allowlist to succeed: %v", err) + } + if monitoringSnapshot.Point.Timestamp <= 0 { + t.Fatalf("unexpected monitor snapshot point: %#v", monitoringSnapshot.Point) + } + children, err := provider.ListResources(context.Background(), cfg, mbean.Path) if err != nil { t.Fatalf("ListResources(mbean) returned error: %v", err) } modeAttr := findResourceByName(t, children, "Mode") + passwordAttr := findResourceByName(t, children, "Password") + apiKeyAttr := findResourceByName(t, children, "ApiKey") lastInvocationAttr := findResourceByName(t, children, "LastInvocation") resizeOp := findResourceByName(t, children, "resize(int,boolean)") + passwordSnapshot, err := provider.GetValue(context.Background(), cfg, passwordAttr.Path) + if err != nil { + t.Fatalf("GetValue(password) returned error: %v", err) + } + if !passwordSnapshot.Sensitive { + t.Fatalf("expected password snapshot to be sensitive: %#v", passwordSnapshot) + } + for _, action := range passwordSnapshot.SupportedActions { + if payloadValue, ok := action.PayloadExample["value"]; ok && payloadValue == "secret-token" { + t.Fatalf("sensitive payload example leaked raw password: %#v", action.PayloadExample) + } + } + + apiKeySnapshot, err := provider.GetValue(context.Background(), cfg, apiKeyAttr.Path) + if err != nil { + t.Fatalf("GetValue(api key) returned error: %v", err) + } + if !apiKeySnapshot.Sensitive { + t.Fatalf("expected api key snapshot to be sensitive: %#v", apiKeySnapshot) + } + for _, action := range apiKeySnapshot.SupportedActions { + if payloadValue, ok := action.PayloadExample["value"]; ok && payloadValue == "api-key-secret" { + t.Fatalf("sensitive payload example leaked raw api key: %#v", action.PayloadExample) + } + } + modeBefore, err := provider.GetValue(context.Background(), cfg, modeAttr.Path) if err != nil { t.Fatalf("GetValue(mode before) returned error: %v", err) diff --git a/internal/jvm/jmxhelper_assets/jmx-helper-runtime.jar b/internal/jvm/jmxhelper_assets/jmx-helper-runtime.jar index 396bbc6a0c76a8e5ed5351d73adbf9f0f3be7b9e..6214d155f27b2dbcbcbcc6c58faf5186f60e3e8c 100644 GIT binary patch delta 20593 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 jQVHe6It8^Bwc9h7|?WBZkMixgTe|PIo;1c6scC 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 diff --git a/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/CacheSettings.java b/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/CacheSettings.java index b2393d6..c561c10 100644 --- a/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/CacheSettings.java +++ b/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/CacheSettings.java @@ -2,6 +2,8 @@ package com.gonavi.fixture; public final class CacheSettings implements CacheSettingsMBean { private volatile String mode = "warm"; + private volatile String password = "secret-token"; + private volatile String apiKey = "api-key-secret"; private final int hitCount = 7; private volatile String lastInvocation = "none"; @@ -15,6 +17,26 @@ public final class CacheSettings implements CacheSettingsMBean { this.mode = mode; } + @Override + public String getPassword() { + return password; + } + + @Override + public void setPassword(String password) { + this.password = password; + } + + @Override + public String getApiKey() { + return apiKey; + } + + @Override + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + @Override public int getHitCount() { return hitCount; diff --git a/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/CacheSettingsMBean.java b/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/CacheSettingsMBean.java index 4673c8b..c954e44 100644 --- a/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/CacheSettingsMBean.java +++ b/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/CacheSettingsMBean.java @@ -4,6 +4,12 @@ public interface CacheSettingsMBean { String getMode(); void setMode(String mode); + String getPassword(); + void setPassword(String password); + + String getApiKey(); + void setApiKey(String apiKey); + int getHitCount(); String getLastInvocation(); diff --git a/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/JMXTestServer.java b/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/JMXTestServer.java index 2fb1bf3..b3f2929 100644 --- a/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/JMXTestServer.java +++ b/internal/jvm/testdata/jmxfixture/src/com/gonavi/fixture/JMXTestServer.java @@ -15,6 +15,14 @@ public final class JMXTestServer { if (!server.isRegistered(objectName)) { server.registerMBean(new CacheSettings(), objectName); } + ObjectName defaultDomainObjectName = new ObjectName(":type=CacheSettings,name=DefaultDomainCache"); + if (!server.isRegistered(defaultDomainObjectName)) { + server.registerMBean(new CacheSettings(), defaultDomainObjectName); + } + ObjectName whitespaceDomainObjectName = new ObjectName("com.gonavi.fixture :type=CacheSettings,name=WhitespaceDomainCache"); + if (!server.isRegistered(whitespaceDomainObjectName)) { + server.registerMBean(new CacheSettings(), whitespaceDomainObjectName); + } System.out.println("READY"); System.out.flush(); diff --git a/tools/jmx-helper/src/com/gonavi/jmxhelper/JmxRuntime.java b/tools/jmx-helper/src/com/gonavi/jmxhelper/JmxRuntime.java index 46e1540..ba349d4 100644 --- a/tools/jmx-helper/src/com/gonavi/jmxhelper/JmxRuntime.java +++ b/tools/jmx-helper/src/com/gonavi/jmxhelper/JmxRuntime.java @@ -52,13 +52,13 @@ final class JmxRuntime { case "list": return listResources(server, connection, target); case "get": - return singleton("snapshot", getValue(server, target)); + return singleton("snapshot", getValue(server, connection, target)); case "monitor": - return singleton("monitoringSnapshot", getMonitoringSnapshot(server)); + return singleton("monitoringSnapshot", getMonitoringSnapshot(server, connection)); case "preview": - return singleton("preview", previewChange(server, target, change)); + return singleton("preview", previewChange(server, connection, target, change)); case "apply": - return singleton("applyResult", applyChange(server, target, change)); + return singleton("applyResult", applyChange(server, connection, target, change)); default: throw new IllegalArgumentException("unsupported helper command: " + command); } @@ -100,6 +100,7 @@ final class JmxRuntime { } if (target.isDomain()) { + requireDomainAllowed(connection, target.domain); Set names = server.queryNames(new ObjectName(target.domain + ":*"), null); List sortedNames = new ArrayList<>(names); Collections.sort(sortedNames, Comparator.comparing(ObjectName::getCanonicalName)); @@ -123,6 +124,7 @@ final class JmxRuntime { if (target.isMBean()) { ObjectName objectName = new ObjectName(target.objectName); + requireDomainAllowed(connection, objectName); MBeanInfo info = server.getMBeanInfo(objectName); MBeanAttributeInfo[] attributes = info.getAttributes(); @@ -170,10 +172,15 @@ final class JmxRuntime { throw new IllegalArgumentException("target kind " + target.kind + " does not support list"); } - private static Map getValue(MBeanServerConnection server, TargetSpec target) throws Exception { + private static Map getValue( + MBeanServerConnection server, + ConnectionSpec connection, + TargetSpec target + ) throws Exception { requireTarget(target); if (target.isDomain()) { + requireDomainAllowed(connection, target.domain); Set names = server.queryNames(new ObjectName(target.domain + ":*"), null); Map value = new LinkedHashMap<>(); value.put("domain", target.domain); @@ -182,6 +189,7 @@ final class JmxRuntime { } ObjectName objectName = new ObjectName(target.objectName); + requireDomainAllowed(connection, objectName); if (target.isMBean()) { MBeanInfo info = server.getMBeanInfo(objectName); List> attributes = new ArrayList<>(); @@ -219,7 +227,9 @@ final class JmxRuntime { throw new IllegalArgumentException("unsupported target kind: " + target.kind); } - private static Map getMonitoringSnapshot(MBeanServerConnection server) throws Exception { + private static Map getMonitoringSnapshot(MBeanServerConnection server, ConnectionSpec connection) throws Exception { + requireDomainAllowed(connection, "java.lang"); + LinkedHashMap result = new LinkedHashMap<>(); LinkedHashMap point = new LinkedHashMap<>(); List availableMetrics = new ArrayList<>(); @@ -423,6 +433,7 @@ final class JmxRuntime { private static Map previewChange( MBeanServerConnection server, + ConnectionSpec connection, TargetSpec target, Map change ) throws Exception { @@ -431,6 +442,7 @@ final class JmxRuntime { if (target.isAttribute()) { ObjectName objectName = new ObjectName(target.objectName); + requireDomainAllowed(connection, objectName); MBeanAttributeInfo attributeInfo = requireAttributeInfo(server, objectName, target.attribute); Map before = attributeSnapshot(objectName, attributeInfo, server.getAttribute(objectName, target.attribute)); if (!attributeInfo.isWritable()) { @@ -457,6 +469,7 @@ final class JmxRuntime { if (target.isOperation()) { ObjectName objectName = new ObjectName(target.objectName); + requireDomainAllowed(connection, objectName); MBeanOperationInfo operationInfo = requireOperationInfo(server, objectName, target.operation, target.signature); List args = argumentList(payload); String[] signature = effectiveSignature(target, payload, operationInfo); @@ -486,6 +499,7 @@ final class JmxRuntime { private static Map applyChange( MBeanServerConnection server, + ConnectionSpec connection, TargetSpec target, Map change ) throws Exception { @@ -494,6 +508,7 @@ final class JmxRuntime { if (target.isAttribute()) { ObjectName objectName = new ObjectName(target.objectName); + requireDomainAllowed(connection, objectName); MBeanAttributeInfo attributeInfo = requireAttributeInfo(server, objectName, target.attribute); if (!attributeInfo.isWritable()) { throw new IllegalArgumentException("attribute " + target.attribute + " is not writable"); @@ -512,6 +527,7 @@ final class JmxRuntime { if (target.isOperation()) { ObjectName objectName = new ObjectName(target.objectName); + requireDomainAllowed(connection, objectName); MBeanOperationInfo operationInfo = requireOperationInfo(server, objectName, target.operation, target.signature); List args = argumentList(payload); String[] signature = effectiveSignature(target, payload, operationInfo); @@ -590,17 +606,18 @@ final class JmxRuntime { Object value ) { Object jsonValue = toJsonCompatible(value); + boolean sensitive = isSensitiveName(attributeInfo.getName()); List> supportedActions = attributeInfo.isWritable() ? Collections.singletonList(actionDefinition( "set", "设置属性", "更新 JMX 属性 " + attributeInfo.getName(), - isSensitiveName(attributeInfo.getName()), + sensitive, Collections.singletonList(payloadField("value", attributeInfo.getType(), true, "目标属性值")), - metadata("value", jsonValue) + sensitive ? Collections.emptyMap() : metadata("value", jsonValue) )) : Collections.emptyList(); - return snapshot("attribute", inferFormat(jsonValue), jsonValue, attributeInfo.getDescription(), isSensitiveName(attributeInfo.getName()), supportedActions, metadata( + return snapshot("attribute", inferFormat(jsonValue), jsonValue, attributeInfo.getDescription(), sensitive, supportedActions, metadata( "objectName", objectName.toString(), "attribute", attributeInfo.getName(), "type", attributeInfo.getType(), @@ -898,13 +915,47 @@ final class JmxRuntime { return lowered.contains("password") || lowered.contains("secret") || lowered.contains("token") - || lowered.contains("credential"); + || lowered.contains("credential") + || lowered.contains("apikey") + || lowered.contains("api_key") + || lowered.contains("accesskey") + || lowered.contains("access_key") + || lowered.contains("privatekey") + || lowered.contains("private_key") + || lowered.contains("secretkey") + || lowered.contains("secret_key") + || lowered.contains("authkey") + || lowered.contains("auth_key"); } private static String domainOf(ObjectName objectName) { return objectName.getDomain(); } + private static void requireDomainAllowed(ConnectionSpec connection, String domain) { + if (connection == null) { + return; + } + String rawDomain = domain == null ? "" : domain; + String normalizedDomain = rawDomain.trim(); + if (normalizedDomain.isEmpty()) { + if (connection.hasDomainAllowlist()) { + throw new IllegalArgumentException("domain is not allowed: "); + } + return; + } + if (!rawDomain.equals(normalizedDomain) || !connection.isDomainAllowed(rawDomain)) { + throw new IllegalArgumentException("domain is not allowed: " + normalizedDomain); + } + } + + private static void requireDomainAllowed(ConnectionSpec connection, ObjectName objectName) { + if (objectName == null) { + return; + } + requireDomainAllowed(connection, objectName.getDomain()); + } + private static void requireTarget(TargetSpec target) { if (target == null || target.isRoot()) { throw new IllegalArgumentException("change target is required"); @@ -1263,6 +1314,10 @@ final class JmxRuntime { return new ConnectionSpec(host, port, username, password, allowlist); } + private boolean hasDomainAllowlist() { + return !domainAllowlist.isEmpty(); + } + private boolean isDomainAllowed(String domain) { return domainAllowlist.isEmpty() || domainAllowlist.contains(domain); }