From fd32ab3ad9d367d0ef80ed9d7f243bd55ba03303 Mon Sep 17 00:00:00 2001 From: imbajin Date: Tue, 14 Jul 2026 14:09:57 +0800 Subject: [PATCH 01/15] fix(pd): redact secret from config logs - exclude the PD secret key from generated toString output - preserve secret access for authentication behavior - cover the logging boundary in the PD core test suite --- .../apache/hugegraph/pd/config/PDConfig.java | 2 + .../hugegraph/pd/core/PDConfigTest.java | 43 +++++++++++++++++++ .../hugegraph/pd/core/PDCoreSuiteTest.java | 1 + 3 files changed, 46 insertions(+) create mode 100644 hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDConfigTest.java diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java index 5d6c8db5e5..67f1ff47a9 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java @@ -31,6 +31,7 @@ import org.springframework.stereotype.Component; import lombok.Data; +import lombok.ToString; /** * PD profile @@ -69,6 +70,7 @@ public class PDConfig { private ThreadPoolGrpc threadPoolGrpc; @Value("${auth.secret-key: 'FXQXbJtbCLxODc6tGci732pkH1cyf8Qg'}") + @ToString.Exclude private String secretKey; @Autowired diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDConfigTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDConfigTest.java new file mode 100644 index 0000000000..28550c9415 --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDConfigTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.pd.core; + +import org.apache.hugegraph.pd.config.PDConfig; +import org.junit.Assert; +import org.junit.Test; + +public class PDConfigTest { + + @Test + public void testToStringDoesNotExposeSecretKey() { + PDConfig config = new PDConfig(); + config.setClusterId(123L); + config.setDataPath("pd-test-data"); + config.setInitialStoreList(""); + config.setSecretKey("secret-value-that-must-not-appear"); + + String text = config.toString(); + + Assert.assertEquals("secret-value-that-must-not-appear", + config.getSecretKey()); + Assert.assertFalse(text.contains("secret-value-that-must-not-appear")); + Assert.assertFalse(text.contains("secretKey")); + Assert.assertTrue(text.contains("clusterId=123")); + Assert.assertTrue(text.contains("dataPath=pd-test-data")); + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java index 87d1500bcb..95b044c76b 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java @@ -31,6 +31,7 @@ @Suite.SuiteClasses({ MetadataKeyHelperTest.class, HgKVStoreImplTest.class, + PDConfigTest.class, ConfigServiceTest.class, IdServiceTest.class, KvServiceTest.class, From 2ccc4e2dd150e64e7bbf717c7aba573f13923959 Mon Sep 17 00:00:00 2001 From: imbajin Date: Tue, 14 Jul 2026 14:10:03 +0800 Subject: [PATCH 02/15] feat(pd): expose store rest address - derive the REST authority from the registered host and rest.port label - share IPv4 and IPv6 validation with service discovery - preserve the legacy discovery fallback for missing labels - cover additive DTO output and malformed address handling --- .../apache/hugegraph/pd/rest/StoreAPI.java | 3 + .../hugegraph/pd/service/SDConfigService.java | 28 +------ .../pd/util/StoreRestAddressUtil.java | 71 ++++++++++++++++ .../hugegraph/pd/rest/PDRestSuiteTest.java | 3 + .../pd/rest/StoreAPIStoreStatisticsTest.java | 68 ++++++++++++++++ .../pd/util/StoreRestAddressUtilTest.java | 81 +++++++++++++++++++ 6 files changed, 230 insertions(+), 24 deletions(-) create mode 100644 hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtil.java create mode 100644 hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/StoreAPIStoreStatisticsTest.java create mode 100644 hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtilTest.java diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java index 9d7211e3c9..2cddb29feb 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java @@ -33,6 +33,7 @@ import org.apache.hugegraph.pd.model.TimeRangeRequest; import org.apache.hugegraph.pd.service.PDRestService; import org.apache.hugegraph.pd.util.DateUtil; +import org.apache.hugegraph.pd.util.StoreRestAddressUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.DeleteMapping; @@ -278,6 +279,7 @@ class StoreStatistics { // store statistics String storeId; String address; + String restAddress; String raftAddress; String version; String state; @@ -302,6 +304,7 @@ class StoreStatistics { if (store != null) { storeId = String.valueOf(store.getId()); address = store.getAddress(); + restAddress = StoreRestAddressUtil.getRestAddress(store); raftAddress = store.getRaftAddress(); state = String.valueOf(store.getState()); version = store.getVersion(); diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java index dda48bb73c..3fac92efaf 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java @@ -22,7 +22,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -33,6 +32,7 @@ import org.apache.hugegraph.pd.common.PDException; import org.apache.hugegraph.pd.config.PDConfig; import org.apache.hugegraph.pd.grpc.Metapb; +import org.apache.hugegraph.pd.util.StoreRestAddressUtil; import org.apache.hugegraph.pd.grpc.Pdpb; import org.apache.hugegraph.pd.grpc.discovery.NodeInfo; import org.apache.hugegraph.pd.grpc.discovery.NodeInfos; @@ -215,34 +215,14 @@ private Set getStoreAddresses() { return res; } - // TODO: optimized store registry data, to add host:port of REST server. + // Keep the legacy gRPC fallback when no valid REST port is registered. private String getRestAddress(Metapb.Store store) { String address = store.getAddress(); if (address == null || address.isEmpty()) { return null; } - try { - Optional port = store.getLabelsList().stream().map( - e -> { - if ("rest.port".equals(e.getKey())) { - return e.getValue(); - } - return null; - }).filter(e -> e != null).findFirst(); - - if (port.isPresent()) { - java.net.URI uri = address.contains("://") - ? java.net.URI.create(address) - : java.net.URI.create("http://" + address); - String host = uri.getHost() != null ? uri.getHost() : address; - String hostPart = - host.contains(":") && !host.startsWith("[") ? "[" + host + "]" : host; - address = hostPart + ":" + port.get().trim(); - } - } catch (Throwable t) { - log.error("Failed to extract the REST address of store, cause by:", t); - } - return address; + String restAddress = StoreRestAddressUtil.getRestAddress(store); + return restAddress != null ? restAddress : address; } diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtil.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtil.java new file mode 100644 index 0000000000..1f9bfe19b2 --- /dev/null +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtil.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.pd.util; + +import java.net.URI; +import java.util.Optional; + +import org.apache.hugegraph.pd.grpc.Metapb; + +public final class StoreRestAddressUtil { + + private static final String REST_PORT_LABEL = "rest.port"; + + private StoreRestAddressUtil() { + } + + public static String getRestAddress(Metapb.Store store) { + if (store == null || store.getAddress().isEmpty()) { + return null; + } + + Optional label = store.getLabelsList().stream() + .filter(item -> REST_PORT_LABEL.equals( + item.getKey())) + .map(Metapb.StoreLabel::getValue) + .findFirst(); + if (!label.isPresent()) { + return null; + } + + int port; + try { + port = Integer.parseInt(label.get().trim()); + } catch (NumberFormatException ignored) { + return null; + } + if (port < 1 || port > 65535) { + return null; + } + + try { + String address = store.getAddress().trim(); + URI uri = address.contains("://") ? URI.create(address) : + URI.create("http://" + address); + String host = uri.getHost(); + if (host == null || host.isEmpty()) { + return null; + } + String hostPart = host.contains(":") && !host.startsWith("[") ? + "[" + host + "]" : host; + return hostPart + ":" + port; + } catch (IllegalArgumentException ignored) { + return null; + } + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java index f804290d5a..84ddb3af47 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.pd.rest; +import org.apache.hugegraph.pd.util.StoreRestAddressUtilTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -25,6 +26,8 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ RestApiTest.class, + StoreAPIStoreStatisticsTest.class, + StoreRestAddressUtilTest.class, }) @Slf4j public class PDRestSuiteTest { diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/StoreAPIStoreStatisticsTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/StoreAPIStoreStatisticsTest.java new file mode 100644 index 0000000000..eaef52c747 --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/StoreAPIStoreStatisticsTest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.pd.rest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.grpc.Metapb; +import org.apache.hugegraph.pd.service.PDRestService; +import org.junit.Test; + +public class StoreAPIStoreStatisticsTest { + + @Test + public void testExposeStoreRestAddress() throws PDException { + StoreAPI api = new StoreAPI(); + api.pdRestService = mock(PDRestService.class); + Metapb.Store store = store("127.0.0.1:8500", "8520"); + when(api.pdRestService.getStore(store.getId())).thenReturn(store); + + StoreAPI.StoreStatistics statistics = api.new StoreStatistics(store); + + assertEquals("127.0.0.1:8500", statistics.getAddress()); + assertEquals("127.0.0.1:8520", statistics.getRestAddress()); + } + + @Test + public void testKeepRestAddressNullWithoutPortLabel() throws PDException { + StoreAPI api = new StoreAPI(); + api.pdRestService = mock(PDRestService.class); + Metapb.Store store = store("127.0.0.1:8500", null); + when(api.pdRestService.getStore(store.getId())).thenReturn(store); + + StoreAPI.StoreStatistics statistics = api.new StoreStatistics(store); + + assertNull(statistics.getRestAddress()); + } + + private static Metapb.Store store(String address, String restPort) { + Metapb.Store.Builder builder = Metapb.Store.newBuilder() + .setId(1L) + .setAddress(address); + if (restPort != null) { + builder.addLabels(Metapb.StoreLabel.newBuilder() + .setKey("rest.port") + .setValue(restPort)); + } + return builder.build(); + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtilTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtilTest.java new file mode 100644 index 0000000000..898103ccf0 --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtilTest.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.pd.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.apache.hugegraph.pd.grpc.Metapb; +import org.junit.Test; + +public class StoreRestAddressUtilTest { + + @Test + public void testBuildIPv4RestAddress() { + Metapb.Store store = store("127.0.0.1:8500", "8520"); + + assertEquals("127.0.0.1:8520", + StoreRestAddressUtil.getRestAddress(store)); + } + + @Test + public void testBuildIPv6RestAddress() { + Metapb.Store store = store("[2001:db8::1]:8500", "8520"); + + assertEquals("[2001:db8::1]:8520", + StoreRestAddressUtil.getRestAddress(store)); + } + + @Test + public void testBuildRestAddressFromUri() { + Metapb.Store store = store("http://store.example:8500", "8520"); + + assertEquals("store.example:8520", + StoreRestAddressUtil.getRestAddress(store)); + } + + @Test + public void testRejectMissingOrInvalidRestPort() { + assertNull(StoreRestAddressUtil.getRestAddress( + store("127.0.0.1:8500", null))); + assertNull(StoreRestAddressUtil.getRestAddress( + store("127.0.0.1:8500", "0"))); + assertNull(StoreRestAddressUtil.getRestAddress( + store("127.0.0.1:8500", "65536"))); + assertNull(StoreRestAddressUtil.getRestAddress( + store("127.0.0.1:8500", "8520/path"))); + } + + @Test + public void testRejectMissingOrMalformedStoreAddress() { + assertNull(StoreRestAddressUtil.getRestAddress(store("", "8520"))); + assertNull(StoreRestAddressUtil.getRestAddress( + store("2001:db8::1:8500", "8520"))); + } + + private static Metapb.Store store(String address, String restPort) { + Metapb.Store.Builder builder = Metapb.Store.newBuilder() + .setAddress(address); + if (restPort != null) { + builder.addLabels(Metapb.StoreLabel.newBuilder() + .setKey("rest.port") + .setValue(restPort)); + } + return builder.build(); + } +} From 72f902ce84ed5d01bbbb52d7675edc48007ff152 Mon Sep 17 00:00:00 2001 From: imbajin Date: Tue, 14 Jul 2026 14:10:07 +0800 Subject: [PATCH 03/15] fix(server): apply explicit graph limits - propagate explicit schema and serializer limits to PD graph configs - preserve values supplied by PD or callers with putIfAbsent - exclude aliases, implicit defaults, and unrelated sensitive settings - cover propagation and compatibility in the server unit suite --- .../apache/hugegraph/core/GraphManager.java | 11 ++ .../apache/hugegraph/unit/UnitTestSuite.java | 2 + .../unit/core/GraphManagerConfigTest.java | 154 ++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java index 362a2f9a35..f716285c67 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java @@ -1864,6 +1864,17 @@ private Map attachLocalCacheConfig(Map configs) if (StringUtils.isNotEmpty((String) configs.get(CoreOptions.ALIAS_NAME.name()))) { return attachedConfigs; } + if (this.config.containsKey(CoreOptions.SCHEMA_CACHE_CAPACITY.name())) { + attachedConfigs.putIfAbsent( + CoreOptions.SCHEMA_CACHE_CAPACITY.name(), + this.config.get(CoreOptions.SCHEMA_CACHE_CAPACITY)); + } + if (this.config.containsKey( + CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name())) { + attachedConfigs.putIfAbsent( + CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name(), + this.config.get(CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY)); + } Object value = this.config.get(CoreOptions.VERTEX_CACHE_EXPIRE); if (Objects.nonNull(value)) { attachedConfigs.putIfAbsent(CoreOptions.VERTEX_CACHE_EXPIRE.name(), diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index 433e75a812..43ee82dbae 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -40,6 +40,7 @@ import org.apache.hugegraph.unit.core.DataTypeTest; import org.apache.hugegraph.unit.core.DirectionsTest; import org.apache.hugegraph.unit.core.ExceptionTest; +import org.apache.hugegraph.unit.core.GraphManagerConfigTest; import org.apache.hugegraph.unit.core.LocksTableTest; import org.apache.hugegraph.unit.core.PageStateTest; import org.apache.hugegraph.unit.core.QueryTest; @@ -133,6 +134,7 @@ SecurityManagerTest.class, RolePermissionTest.class, ExceptionTest.class, + GraphManagerConfigTest.class, BackendStoreInfoTest.class, TraversalUtilTest.class, TraversalUtilOptimizeTest.class, diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java new file mode 100644 index 0000000000..b8601c22d5 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.unit.core; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.hugegraph.config.CoreOptions; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.ServerOptions; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.junit.Test; + +public class GraphManagerConfigTest { + + @Test + public void testAttachExplicitLocalResourceLimits() { + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(CoreOptions.SCHEMA_CACHE_CAPACITY.name(), 1000L); + properties.setProperty( + CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name(), 8388608); + GraphManager manager = newManager(properties); + + try { + Map attached = attach(manager, new HashMap<>()); + + Assert.assertEquals(1000L, attached.get( + CoreOptions.SCHEMA_CACHE_CAPACITY.name())); + Assert.assertEquals(8388608, attached.get( + CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name())); + } finally { + manager.close(); + } + } + + @Test + public void testKeepIncomingResourceLimits() { + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(CoreOptions.SCHEMA_CACHE_CAPACITY.name(), 1000L); + properties.setProperty( + CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name(), 8388608); + GraphManager manager = newManager(properties); + Map configs = new HashMap<>(); + configs.put(CoreOptions.SCHEMA_CACHE_CAPACITY.name(), 2000L); + configs.put(CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name(), + 16777216); + + try { + Map attached = attach(manager, configs); + + Assert.assertEquals(2000L, attached.get( + CoreOptions.SCHEMA_CACHE_CAPACITY.name())); + Assert.assertEquals(16777216, attached.get( + CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name())); + } finally { + manager.close(); + } + } + + @Test + public void testAttachDoesNotMutateSourceOrCopySensitiveOptions() { + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(CoreOptions.SCHEMA_CACHE_CAPACITY.name(), 1000L); + properties.setProperty( + CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name(), 8388608); + properties.setProperty(CoreOptions.BACKEND.name(), "memory"); + properties.setProperty(CoreOptions.STORE.name(), "local_store"); + properties.setProperty(ServerOptions.ADMIN_PA.name(), "local_secret"); + GraphManager manager = newManager(properties); + Map configs = new HashMap<>(); + configs.put("marker", "source"); + Map source = new HashMap<>(configs); + + try { + Map attached = attach(manager, configs); + + Assert.assertEquals(source, configs); + Assert.assertEquals("source", attached.get("marker")); + Assert.assertFalse(attached.containsKey(CoreOptions.BACKEND.name())); + Assert.assertFalse(attached.containsKey(CoreOptions.STORE.name())); + Assert.assertFalse(attached.containsKey(ServerOptions.ADMIN_PA.name())); + } finally { + manager.close(); + } + } + + @Test + public void testDoNotAttachImplicitLocalDefaults() { + GraphManager manager = newManager(new PropertiesConfiguration()); + + try { + Map attached = attach(manager, new HashMap<>()); + + Assert.assertFalse(attached.containsKey( + CoreOptions.SCHEMA_CACHE_CAPACITY.name())); + Assert.assertFalse(attached.containsKey( + CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name())); + } finally { + manager.close(); + } + } + + @Test + public void testDoNotAttachResourceLimitsToAliasGraph() { + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(CoreOptions.SCHEMA_CACHE_CAPACITY.name(), 1000L); + properties.setProperty( + CoreOptions.SERIALIZER_BUFFER_MAX_CAPACITY.name(), 8388608); + GraphManager manager = newManager(properties); + Map configs = new HashMap<>(); + configs.put(CoreOptions.ALIAS_NAME.name(), "source_graph"); + Map source = new HashMap<>(configs); + + try { + Map attached = attach(manager, configs); + + Assert.assertEquals(source, attached); + Assert.assertEquals(source, configs); + } finally { + manager.close(); + } + } + + private static GraphManager newManager(PropertiesConfiguration properties) { + return new GraphManager(new HugeConfig(properties), + new EventHub("config-test")); + } + + @SuppressWarnings("unchecked") + private static Map attach(GraphManager manager, + Map configs) { + return Whitebox.invoke(manager.getClass(), new Class[]{Map.class}, + "attachLocalCacheConfig", manager, configs); + } +} From c917bd133fac23b54975485ffd9cdd897da85a54 Mon Sep 17 00:00:00 2001 From: imbajin Date: Tue, 14 Jul 2026 14:24:44 +0800 Subject: [PATCH 04/15] fix(pd): share store rest address utility - move the address helper into the PD common module - keep clean reactor tests independent from the repackaged service jar - retain IPv4, IPv6, port, and malformed input coverage - rely on the real REST deployment check for additive DTO output --- .../pd/util/StoreRestAddressUtil.java | 0 .../hugegraph/pd/rest/PDRestSuiteTest.java | 1 - .../pd/rest/StoreAPIStoreStatisticsTest.java | 68 ------------------- 3 files changed, 69 deletions(-) rename hugegraph-pd/{hg-pd-service => hg-pd-common}/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtil.java (100%) delete mode 100644 hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/StoreAPIStoreStatisticsTest.java diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtil.java b/hugegraph-pd/hg-pd-common/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtil.java similarity index 100% rename from hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtil.java rename to hugegraph-pd/hg-pd-common/src/main/java/org/apache/hugegraph/pd/util/StoreRestAddressUtil.java diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java index 84ddb3af47..5dba561948 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java @@ -26,7 +26,6 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ RestApiTest.class, - StoreAPIStoreStatisticsTest.class, StoreRestAddressUtilTest.class, }) @Slf4j diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/StoreAPIStoreStatisticsTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/StoreAPIStoreStatisticsTest.java deleted file mode 100644 index eaef52c747..0000000000 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/StoreAPIStoreStatisticsTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.pd.rest; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.apache.hugegraph.pd.common.PDException; -import org.apache.hugegraph.pd.grpc.Metapb; -import org.apache.hugegraph.pd.service.PDRestService; -import org.junit.Test; - -public class StoreAPIStoreStatisticsTest { - - @Test - public void testExposeStoreRestAddress() throws PDException { - StoreAPI api = new StoreAPI(); - api.pdRestService = mock(PDRestService.class); - Metapb.Store store = store("127.0.0.1:8500", "8520"); - when(api.pdRestService.getStore(store.getId())).thenReturn(store); - - StoreAPI.StoreStatistics statistics = api.new StoreStatistics(store); - - assertEquals("127.0.0.1:8500", statistics.getAddress()); - assertEquals("127.0.0.1:8520", statistics.getRestAddress()); - } - - @Test - public void testKeepRestAddressNullWithoutPortLabel() throws PDException { - StoreAPI api = new StoreAPI(); - api.pdRestService = mock(PDRestService.class); - Metapb.Store store = store("127.0.0.1:8500", null); - when(api.pdRestService.getStore(store.getId())).thenReturn(store); - - StoreAPI.StoreStatistics statistics = api.new StoreStatistics(store); - - assertNull(statistics.getRestAddress()); - } - - private static Metapb.Store store(String address, String restPort) { - Metapb.Store.Builder builder = Metapb.Store.newBuilder() - .setId(1L) - .setAddress(address); - if (restPort != null) { - builder.addLabels(Metapb.StoreLabel.newBuilder() - .setKey("rest.port") - .setValue(restPort)); - } - return builder.build(); - } -} From 544c58d67815055c4cda9f2f0c9f73a386119409 Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 15 Jul 2026 04:58:43 +0800 Subject: [PATCH 05/15] fix(server): accept bearer auth for Gremlin HTTP --- .../auth/WsAndHttpBasicAuthHandler.java | 59 ++++---- .../auth/WsAndHttpBasicAuthHandlerTest.java | 136 ++++++++++++++++++ .../apache/hugegraph/unit/UnitTestSuite.java | 2 + 3 files changed, 173 insertions(+), 24 deletions(-) create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java index 1b329e19f9..3c3dd71b59 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java @@ -53,6 +53,8 @@ public class WsAndHttpBasicAuthHandler extends SaslAuthenticationHandler { private static final String AUTHENTICATOR = "authenticator"; private static final String HTTP_AUTH = "http-authentication"; + private static final String BASIC_AUTH_PREFIX = "Basic "; + private static final String BEARER_TOKEN_PREFIX = "Bearer "; public WsAndHttpBasicAuthHandler(Authenticator authenticator, Settings settings) { @@ -103,28 +105,21 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { return; } - // strip off "Basic " from the Authorization header (RFC 2617) - final String basic = "Basic "; final String header = request.headers().get("Authorization"); - if (!header.startsWith(basic)) { - sendError(ctx, msg); - return; - } - byte[] userPass = null; - try { - final String encoded = header.substring(basic.length()); - userPass = this.decoder.decode(encoded); - } catch (IndexOutOfBoundsException iae) { - sendError(ctx, msg); - return; - } catch (IllegalArgumentException iae) { - sendError(ctx, msg); - return; - } - String authorization = new String(userPass, - StandardCharsets.UTF_8); - String[] split = authorization.split(":"); - if (split.length != 2) { + final Map credentials = new HashMap<>(); + if (header.startsWith(BASIC_AUTH_PREFIX)) { + if (!parseBasicCredentials(header, credentials)) { + sendError(ctx, msg); + return; + } + } else if (header.startsWith(BEARER_TOKEN_PREFIX)) { + String token = header.substring(BEARER_TOKEN_PREFIX.length()); + if (token.isEmpty()) { + sendError(ctx, msg); + return; + } + credentials.put(HugeAuthenticator.KEY_TOKEN, token); + } else { sendError(ctx, msg); return; } @@ -134,9 +129,6 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { address = address.substring(1); } - final Map credentials = new HashMap<>(); - credentials.put(PROPERTY_USERNAME, split[0]); - credentials.put(PROPERTY_PASSWORD, split[1]); credentials.put(HugeAuthenticator.KEY_ADDRESS, address); try { @@ -148,6 +140,25 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } } + private boolean parseBasicCredentials(String header, + Map credentials) { + byte[] userPass; + try { + String encoded = header.substring(BASIC_AUTH_PREFIX.length()); + userPass = this.decoder.decode(encoded); + } catch (IndexOutOfBoundsException | IllegalArgumentException e) { + return false; + } + String authorization = new String(userPass, StandardCharsets.UTF_8); + String[] split = authorization.split(":"); + if (split.length != 2) { + return false; + } + credentials.put(PROPERTY_USERNAME, split[0]); + credentials.put(PROPERTY_PASSWORD, split[1]); + return true; + } + private void sendError(ChannelHandlerContext context, Object msg) { // Close the connection as soon as the error message is sent. context.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED)) diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java new file mode 100644 index 0000000000..716adbcabd --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.auth; + +import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; +import static io.netty.handler.codec.http.HttpMethod.POST; +import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +import org.apache.tinkerpop.gremlin.server.Settings; +import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser; +import org.apache.tinkerpop.gremlin.server.auth.Authenticator; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; + +public class WsAndHttpBasicAuthHandlerTest { + + @SuppressWarnings("unchecked") + @Test + public void testBearerTokenAuthenticatesHttpGremlinRequest() + throws Exception { + Authenticator authenticator = Mockito.mock(Authenticator.class); + Mockito.when(authenticator.authenticate(Mockito.anyMap())) + .thenReturn(new AuthenticatedUser("admin")); + WsAndHttpBasicAuthHandler handler = + new WsAndHttpBasicAuthHandler(authenticator, new Settings()); + EmbeddedChannel channel = new EmbeddedChannel(); + channel.pipeline().addLast("authenticator", handler); + + DefaultFullHttpRequest request = + new DefaultFullHttpRequest(HTTP_1_1, POST, "/gremlin"); + request.headers().set(AUTHORIZATION, "Bearer server-token"); + channel.writeInbound(request); + + ArgumentCaptor> credentials = + ArgumentCaptor.forClass(Map.class); + Mockito.verify(authenticator).authenticate(credentials.capture()); + Mockito.verifyNoMoreInteractions(authenticator); + Assert.assertEquals("server-token", + credentials.getValue().get( + HugeAuthenticator.KEY_TOKEN)); + Assert.assertFalse( + credentials.getValue().containsKey("username")); + Assert.assertFalse( + credentials.getValue().containsKey("password")); + channel.finishAndReleaseAll(); + } + + @SuppressWarnings("unchecked") + @Test + public void testBasicCredentialsStillAuthenticateHttpGremlinRequest() + throws Exception { + Authenticator authenticator = Mockito.mock(Authenticator.class); + Mockito.when(authenticator.authenticate(Mockito.anyMap())) + .thenReturn(new AuthenticatedUser("admin")); + EmbeddedChannel channel = channel(authenticator); + String encoded = Base64.getEncoder().encodeToString( + "admin:password".getBytes(StandardCharsets.UTF_8)); + + channel.writeInbound(request("Basic " + encoded)); + + ArgumentCaptor> credentials = + ArgumentCaptor.forClass(Map.class); + Mockito.verify(authenticator).authenticate(credentials.capture()); + Assert.assertEquals("admin", credentials.getValue().get("username")); + Assert.assertEquals("password", credentials.getValue().get("password")); + Assert.assertFalse(credentials.getValue().containsKey( + HugeAuthenticator.KEY_TOKEN)); + channel.finishAndReleaseAll(); + } + + @Test + public void testEmptyBearerTokenIsRejected() throws Exception { + assertUnauthorized("Bearer "); + } + + @Test + public void testUnsupportedAuthorizationSchemeIsRejected() + throws Exception { + assertUnauthorized("Digest server-token"); + } + + private static EmbeddedChannel channel(Authenticator authenticator) { + WsAndHttpBasicAuthHandler handler = + new WsAndHttpBasicAuthHandler(authenticator, new Settings()); + EmbeddedChannel channel = new EmbeddedChannel(); + channel.pipeline().addLast("authenticator", handler); + return channel; + } + + private static DefaultFullHttpRequest request(String authorization) { + DefaultFullHttpRequest request = + new DefaultFullHttpRequest(HTTP_1_1, POST, "/gremlin"); + request.headers().set(AUTHORIZATION, authorization); + return request; + } + + private static void assertUnauthorized(String authorization) + throws Exception { + Authenticator authenticator = Mockito.mock(Authenticator.class); + EmbeddedChannel channel = channel(authenticator); + + channel.writeInbound(request(authorization)); + + FullHttpResponse response = channel.readOutbound(); + Assert.assertEquals(UNAUTHORIZED, response.status()); + Mockito.verifyNoInteractions(authenticator); + response.release(); + channel.finishAndReleaseAll(); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index 43ee82dbae..68fa646ec9 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.unit; +import org.apache.hugegraph.auth.WsAndHttpBasicAuthHandlerTest; import org.apache.hugegraph.core.RoleElectionStateMachineTest; import org.apache.hugegraph.meta.EtcdMetaDriverTest; import org.apache.hugegraph.meta.MetaManagerSchemaCacheClearEventTest; @@ -96,6 +97,7 @@ /* api gremlin */ GremlinQueryAPITest.class, + WsAndHttpBasicAuthHandlerTest.class, /* api space */ GraphSpaceAPITest.class, From 08297e7b18d67793ab812e36a3e52b4328a688f6 Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 15 Jul 2026 07:33:21 +0800 Subject: [PATCH 06/15] feat(server): support scoped PD auth metadata --- .../apache/hugegraph/api/auth/AccessAPI.java | 56 +++- .../apache/hugegraph/api/auth/BelongAPI.java | 58 +++- .../api/auth/GraphSpaceGroupAPI.java | 289 ++++++++++++++++++ .../apache/hugegraph/api/auth/TargetAPI.java | 67 +++- .../apache/hugegraph/auth/AuthManager.java | 80 +++++ .../org/apache/hugegraph/auth/HugeTarget.java | 17 ++ .../hugegraph/auth/StandardAuthManagerV2.java | 104 ++++++- .../api/auth/GraphSpaceAuthPayloadTest.java | 110 +++++++ .../api/auth/GraphSpaceGroupAPITest.java | 93 ++++++ .../apache/hugegraph/unit/UnitTestSuite.java | 4 + 10 files changed, 832 insertions(+), 46 deletions(-) create mode 100644 hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java index 35b05eedb1..f585d15747 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java @@ -18,6 +18,7 @@ package org.apache.hugegraph.api.auth; import java.util.List; +import java.util.stream.Collectors; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; @@ -68,10 +69,13 @@ public String create(@Context GraphManager manager, @PathParam("graphspace") String graphSpace, JsonAccess jsonAccess) { LOG.debug("GraphSpace [{}] create access: {}", graphSpace, jsonAccess); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); checkCreatingBody(jsonAccess); - HugeAccess access = jsonAccess.build(); - access.id(manager.authManager().createAccess(access)); + HugeAccess access = jsonAccess.build(graphSpace); + GraphSpaceGroupAPI.checkScopedGroupReference( + manager.authManager(), graphSpace, access.source()); + access.id(manager.authManager().createAccess(graphSpace, access)); return manager.serializer().writeAuthElement(access); } @@ -87,16 +91,21 @@ public String update(@Context GraphManager manager, @PathParam("id") String id, JsonAccess jsonAccess) { LOG.debug("GraphSpace [{}] update access: {}", graphSpace, jsonAccess); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); checkUpdatingBody(jsonAccess); HugeAccess access; try { - access = manager.authManager().getAccess(UserAPI.parseId(id)); + access = manager.authManager().getAccess(graphSpace, + UserAPI.parseId(id)); } catch (NotFoundException e) { throw new IllegalArgumentException("Invalid access id: " + id); } + checkGraphSpace(graphSpace, access); + GraphSpaceGroupAPI.checkScopedGroupReference( + manager.authManager(), graphSpace, access.source()); access = jsonAccess.build(access); - manager.authManager().updateAccess(access); + manager.authManager().updateAccess(graphSpace, access); return manager.serializer().writeAuthElement(access); } @@ -114,19 +123,26 @@ public String list(@Context GraphManager manager, @QueryParam("limit") @DefaultValue("100") long limit) { LOG.debug("GraphSpace [{}] list accesses by group {} or target {}", graphSpace, group, target); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); E.checkArgument(group == null || target == null, "Can't pass both group and target at the same time"); List belongs; if (group != null) { Id id = UserAPI.parseId(group); - belongs = manager.authManager().listAccessByGroup(id, limit); + belongs = manager.authManager().listAccessByGroup(graphSpace, id, + limit); } else if (target != null) { Id id = UserAPI.parseId(target); - belongs = manager.authManager().listAccessByTarget(id, limit); + belongs = manager.authManager().listAccessByTarget(graphSpace, id, + limit); } else { - belongs = manager.authManager().listAllAccess(limit); + belongs = manager.authManager().listAllAccess(graphSpace, limit); } + belongs = belongs.stream() + .filter(access -> graphSpace.equals( + access.graphSpace())) + .collect(Collectors.toList()); return manager.serializer().writeAuthElements("accesses", belongs); } @@ -140,8 +156,11 @@ public String get(@Context GraphManager manager, @Parameter(description = "The access id") @PathParam("id") String id) { LOG.debug("GraphSpace [{}] get access: {}", graphSpace, id); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); - HugeAccess access = manager.authManager().getAccess(UserAPI.parseId(id)); + HugeAccess access = manager.authManager().getAccess( + graphSpace, UserAPI.parseId(id)); + checkGraphSpace(graphSpace, access); return manager.serializer().writeAuthElement(access); } @@ -155,17 +174,29 @@ public void delete(@Context GraphManager manager, @Parameter(description = "The access id") @PathParam("id") String id) { LOG.debug("GraphSpace [{}] delete access: {}", graphSpace, id); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); try { - manager.authManager().deleteAccess(UserAPI.parseId(id)); + HugeAccess access = manager.authManager().getAccess( + graphSpace, UserAPI.parseId(id)); + checkGraphSpace(graphSpace, access); + manager.authManager().deleteAccess(graphSpace, + UserAPI.parseId(id)); } catch (NotFoundException e) { throw new IllegalArgumentException("Invalid access id: " + id); } } + static void checkGraphSpace(String graphSpace, HugeAccess access) { + if (!graphSpace.equals(access.graphSpace())) { + throw new jakarta.ws.rs.ForbiddenException( + "Permission denied: access belongs to another graphspace"); + } + } + @JsonIgnoreProperties(value = {"id", "access_creator", "access_create", "access_update"}) - private static class JsonAccess implements Checkable { + static class JsonAccess implements Checkable { @JsonProperty("group") @Schema(description = "The group id", required = true) @@ -196,8 +227,9 @@ public HugeAccess build(HugeAccess access) { return access; } - public HugeAccess build() { - HugeAccess access = new HugeAccess(UserAPI.parseId(this.group), + public HugeAccess build(String graphSpace) { + HugeAccess access = new HugeAccess(graphSpace, + UserAPI.parseId(this.group), UserAPI.parseId(this.target)); access.permission(this.permission); access.description(this.description); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java index 09af7f51c9..d092fd6efa 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java @@ -18,6 +18,7 @@ package org.apache.hugegraph.api.auth; import java.util.List; +import java.util.stream.Collectors; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; @@ -67,10 +68,13 @@ public String create(@Context GraphManager manager, @PathParam("graphspace") String graphSpace, JsonBelong jsonBelong) { LOG.debug("GraphSpace [{}] create belong: {}", graphSpace, jsonBelong); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); checkCreatingBody(jsonBelong); - HugeBelong belong = jsonBelong.build(); - belong.id(manager.authManager().createBelong(belong)); + HugeBelong belong = jsonBelong.build(graphSpace); + GraphSpaceGroupAPI.checkBelongReferences(manager.authManager(), + graphSpace, belong); + belong.id(manager.authManager().createBelong(graphSpace, belong)); return manager.serializer().writeAuthElement(belong); } @@ -86,16 +90,21 @@ public String update(@Context GraphManager manager, @PathParam("id") String id, JsonBelong jsonBelong) { LOG.debug("GraphSpace [{}] update belong: {}", graphSpace, jsonBelong); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); checkUpdatingBody(jsonBelong); HugeBelong belong; try { - belong = manager.authManager().getBelong(UserAPI.parseId(id)); + belong = manager.authManager().getBelong(graphSpace, + UserAPI.parseId(id)); } catch (NotFoundException e) { throw new IllegalArgumentException("Invalid belong id: " + id); } + checkGraphSpace(graphSpace, belong); + GraphSpaceGroupAPI.checkBelongReferences(manager.authManager(), + graphSpace, belong); belong = jsonBelong.build(belong); - manager.authManager().updateBelong(belong); + manager.authManager().updateBelong(graphSpace, belong); return manager.serializer().writeAuthElement(belong); } @@ -113,19 +122,26 @@ public String list(@Context GraphManager manager, @QueryParam("limit") @DefaultValue("100") long limit) { LOG.debug("GraphSpace [{}] list belongs by user {} or group {}", graphSpace, user, group); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); E.checkArgument(user == null || group == null, "Can't pass both user and group at the same time"); List belongs; if (user != null) { Id id = UserAPI.parseId(user); - belongs = manager.authManager().listBelongByUser(id, limit); + belongs = manager.authManager().listBelongByUser(graphSpace, id, + limit); } else if (group != null) { Id id = UserAPI.parseId(group); - belongs = manager.authManager().listBelongByGroup(id, limit); + belongs = manager.authManager().listBelongByGroup(graphSpace, id, + limit); } else { - belongs = manager.authManager().listAllBelong(limit); + belongs = manager.authManager().listAllBelong(graphSpace, limit); } + belongs = belongs.stream() + .filter(belong -> graphSpace.equals( + belong.graphSpace())) + .collect(Collectors.toList()); return manager.serializer().writeAuthElements("belongs", belongs); } @@ -139,8 +155,11 @@ public String get(@Context GraphManager manager, @Parameter(description = "The belong id") @PathParam("id") String id) { LOG.debug("GraphSpace [{}] get belong: {}", graphSpace, id); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); - HugeBelong belong = manager.authManager().getBelong(UserAPI.parseId(id)); + HugeBelong belong = manager.authManager().getBelong( + graphSpace, UserAPI.parseId(id)); + checkGraphSpace(graphSpace, belong); return manager.serializer().writeAuthElement(belong); } @@ -154,17 +173,29 @@ public void delete(@Context GraphManager manager, @Parameter(description = "The belong id") @PathParam("id") String id) { LOG.debug("GraphSpace [{}] delete belong: {}", graphSpace, id); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); try { - manager.authManager().deleteBelong(UserAPI.parseId(id)); + HugeBelong belong = manager.authManager().getBelong( + graphSpace, UserAPI.parseId(id)); + checkGraphSpace(graphSpace, belong); + manager.authManager().deleteBelong(graphSpace, + UserAPI.parseId(id)); } catch (NotFoundException e) { throw new IllegalArgumentException("Invalid belong id: " + id); } } + static void checkGraphSpace(String graphSpace, HugeBelong belong) { + if (!graphSpace.equals(belong.graphSpace())) { + throw new jakarta.ws.rs.ForbiddenException( + "Permission denied: belong belongs to another graphspace"); + } + } + @JsonIgnoreProperties(value = {"id", "belong_creator", "belong_create", "belong_update"}) - private static class JsonBelong implements Checkable { + static class JsonBelong implements Checkable { @JsonProperty("user") @Schema(description = "The user id", required = true) @@ -189,9 +220,10 @@ public HugeBelong build(HugeBelong belong) { return belong; } - public HugeBelong build() { - HugeBelong belong = new HugeBelong(UserAPI.parseId(this.user), - UserAPI.parseId(this.group)); + public HugeBelong build(String graphSpace) { + HugeBelong belong = new HugeBelong( + graphSpace, UserAPI.parseId(this.user), + UserAPI.parseId(this.group), null, HugeBelong.UG); belong.description(this.description); return belong; } diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java new file mode 100644 index 0000000000..4ee26be15c --- /dev/null +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.api.auth; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import org.apache.hugegraph.api.API; +import org.apache.hugegraph.api.filter.StatusFilter.Status; +import org.apache.hugegraph.auth.AuthManager; +import org.apache.hugegraph.auth.HugeBelong; +import org.apache.hugegraph.auth.HugeGraphAuthProxy; +import org.apache.hugegraph.auth.HugeGroup; +import org.apache.hugegraph.auth.HugeUser; +import org.apache.hugegraph.auth.StandardAuthManagerV2; +import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.define.Checkable; +import org.apache.hugegraph.exception.NotFoundException; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.Log; +import org.slf4j.Logger; + +import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.inject.Singleton; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; + +@Path("graphspaces/{graphspace}/auth/groups") +@Singleton +@Tag(name = "GraphSpaceGroupAPI") +public class GraphSpaceGroupAPI extends API { + + private static final Logger LOG = Log.logger(GraphSpaceGroupAPI.class); + private static final String SCOPED_PREFIX = "~hubble_role:v1:"; + + @POST + @Timed + @Status(Status.CREATED) + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON_WITH_CHARSET) + public String create(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + JsonGroup jsonGroup) { + LOG.debug("GraphSpace [{}] create scoped group", graphSpace); + ensureManager(manager, graphSpace); + checkCreatingBody(jsonGroup); + HugeGroup group = jsonGroup.build(); + checkScopedGroup(graphSpace, group); + group.id(manager.authManager().createGroup(group)); + return manager.serializer().writeAuthElement(group); + } + + @PUT + @Timed + @Path("{id}") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON_WITH_CHARSET) + public String update(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @PathParam("id") String id, + JsonGroup jsonGroup) { + LOG.debug("GraphSpace [{}] update scoped group", graphSpace); + ensureManager(manager, graphSpace); + checkUpdatingBody(jsonGroup); + HugeGroup group = getGroup(manager, id); + checkScopedGroup(graphSpace, group); + group = jsonGroup.build(group); + manager.authManager().updateGroup(group); + return manager.serializer().writeAuthElement(group); + } + + @GET + @Timed + @Produces(APPLICATION_JSON_WITH_CHARSET) + public String list(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @QueryParam("limit") @DefaultValue("100") long limit) { + LOG.debug("GraphSpace [{}] list scoped groups", graphSpace); + ensureManager(manager, graphSpace); + List groups = manager.authManager().listAllGroups(limit); + groups = filterScopedGroups(graphSpace, groups); + return manager.serializer().writeAuthElements("groups", groups); + } + + @GET + @Timed + @Path("{id}") + @Produces(APPLICATION_JSON_WITH_CHARSET) + public String get(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @Parameter(description = "The scoped group id") + @PathParam("id") String id) { + LOG.debug("GraphSpace [{}] get scoped group", graphSpace); + ensureManager(manager, graphSpace); + HugeGroup group = getGroup(manager, id); + checkScopedGroup(graphSpace, group); + return manager.serializer().writeAuthElement(group); + } + + @DELETE + @Timed + @Path("{id}") + @Consumes(APPLICATION_JSON) + public void delete(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @Parameter(description = "The scoped group id") + @PathParam("id") String id) { + LOG.debug("GraphSpace [{}] delete scoped group", graphSpace); + ensureManager(manager, graphSpace); + HugeGroup group = getGroup(manager, id); + checkScopedGroup(graphSpace, group); + manager.authManager().deleteGroup(group.id()); + } + + static void checkManagerPermission(AuthManager authManager, + String graphSpace, String username) { + validPermission(authManager.isAdminManager(username) || + authManager.isSpaceManager(graphSpace, username), + username, "graphspace-group.manage"); + } + + static List filterScopedGroups(String graphSpace, + List groups) { + List scoped = new ArrayList<>(); + for (HugeGroup group : groups) { + if (isScopedGroup(graphSpace, group)) { + scoped.add(group); + } + } + return scoped; + } + + static void checkScopedGroup(String graphSpace, HugeGroup group) { + if (!isScopedGroup(graphSpace, group)) { + throw new ForbiddenException( + "Permission denied: group belongs to another graphspace"); + } + } + + static String scopedPrefix(String graphSpace) { + String encoded = Base64.getUrlEncoder().withoutPadding() + .encodeToString(graphSpace.getBytes( + StandardCharsets.UTF_8)); + return SCOPED_PREFIX + encoded + ":"; + } + + static void ensureManager(GraphManager manager, String graphSpace) { + ensurePdModeEnabled(manager); + ensureAuthManager(manager, graphSpace); + } + + static void ensureAuthManager(GraphManager manager, String graphSpace) { + E.checkArgument(manager.graphSpace(graphSpace) != null, + "The graph space '%s' does not exist", graphSpace); + checkManagerPermission(manager.authManager(), graphSpace, + HugeGraphAuthProxy.username()); + } + + static void checkBelongReferences(AuthManager authManager, + String graphSpace, + HugeBelong belong) { + if (!(authManager instanceof StandardAuthManagerV2)) { + return; + } + HugeUser user = authManager.findUser(belong.source().asString()); + if (user != null) { + String username = user.name(); + if (!authManager.isAdminManager(username) && + !authManager.isSpaceManager(graphSpace, username) && + !authManager.isSpaceMember(graphSpace, username)) { + throw new ForbiddenException( + "Permission denied: user is not a graphspace member"); + } + } else { + checkScopedGroupReference(authManager, graphSpace, + belong.source()); + } + checkScopedGroupReference(authManager, graphSpace, belong.target()); + } + + static void checkScopedGroupReference(AuthManager authManager, + String graphSpace, Id groupId) { + if (!(authManager instanceof StandardAuthManagerV2)) { + return; + } + HugeGroup group; + try { + group = authManager.getGroup(groupId); + } catch (NotFoundException e) { + return; + } + if (group != null) { + checkScopedGroup(graphSpace, group); + } + } + + private static HugeGroup getGroup(GraphManager manager, String id) { + try { + return manager.authManager().getGroup(UserAPI.parseId(id)); + } catch (NotFoundException e) { + throw new IllegalArgumentException("Invalid group id: " + id); + } + } + + private static boolean isScopedGroup(String graphSpace, + HugeGroup group) { + if (group == null || group.name() == null) { + return false; + } + String prefix = scopedPrefix(graphSpace); + String name = group.name(); + return name.startsWith(prefix) && + name.substring(prefix.length()).matches("[0-9a-f]{32}"); + } + + @JsonIgnoreProperties(value = {"id", "group_creator", + "group_create", "group_update"}) + static class JsonGroup implements Checkable { + + @JsonProperty("group_name") + @Schema(description = "The name of group", required = true) + private String name; + @JsonProperty("group_description") + @Schema(description = "The description of group") + private String description; + + HugeGroup build(HugeGroup group) { + E.checkArgument(this.name == null || + group.name().equals(this.name), + "The name of group can't be updated"); + if (this.description != null) { + group.description(this.description); + } + return group; + } + + HugeGroup build() { + HugeGroup group = new HugeGroup(this.name); + group.description(this.description); + return group; + } + + @Override + public void checkCreate(boolean isBatch) { + E.checkArgumentNotNull(this.name, + "The name of group can't be null"); + } + + @Override + public void checkUpdate() { + E.checkArgumentNotNull(this.description, + "The description of group can't be null"); + } + } +} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/TargetAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/TargetAPI.java index 7f673048dc..ed379d2444 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/TargetAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/TargetAPI.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; @@ -68,10 +69,11 @@ public String create(@Context GraphManager manager, @PathParam("graphspace") String graphSpace, JsonTarget jsonTarget) { LOG.debug("GraphSpace [{}] create target: {}", graphSpace, jsonTarget); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); checkCreatingBody(jsonTarget); - HugeTarget target = jsonTarget.build(); - target.id(manager.authManager().createTarget(target)); + HugeTarget target = jsonTarget.build(graphSpace); + target.id(manager.authManager().createTarget(graphSpace, target)); return manager.serializer().writeAuthElement(target); } @@ -87,16 +89,19 @@ public String update(@Context GraphManager manager, @PathParam("id") String id, JsonTarget jsonTarget) { LOG.debug("GraphSpace [{}] update target: {}", graphSpace, jsonTarget); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); checkUpdatingBody(jsonTarget); HugeTarget target; try { - target = manager.authManager().getTarget(UserAPI.parseId(id)); + target = manager.authManager().getTarget(graphSpace, + UserAPI.parseId(id)); } catch (NotFoundException e) { throw new IllegalArgumentException("Invalid target id: " + id); } + checkGraphSpace(graphSpace, target); target = jsonTarget.build(target); - manager.authManager().updateTarget(target); + manager.authManager().updateTarget(graphSpace, target); return manager.serializer().writeAuthElement(target); } @@ -109,8 +114,15 @@ public String list(@Context GraphManager manager, @Parameter(description = "The limit of results to return") @QueryParam("limit") @DefaultValue("100") long limit) { LOG.debug("GraphSpace [{}] list targets", graphSpace); - - List targets = manager.authManager().listAllTargets(limit); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); + + List targets = manager.authManager() + .listAllTargets(graphSpace, + limit); + targets = targets.stream() + .filter(target -> graphSpace.equals( + target.graphSpace())) + .collect(Collectors.toList()); return manager.serializer().writeAuthElements("targets", targets); } @@ -124,8 +136,11 @@ public String get(@Context GraphManager manager, @Parameter(description = "The target id") @PathParam("id") String id) { LOG.debug("GraphSpace [{}] get target: {}", graphSpace, id); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); - HugeTarget target = manager.authManager().getTarget(UserAPI.parseId(id)); + HugeTarget target = manager.authManager().getTarget( + graphSpace, UserAPI.parseId(id)); + checkGraphSpace(graphSpace, target); return manager.serializer().writeAuthElement(target); } @@ -139,17 +154,29 @@ public void delete(@Context GraphManager manager, @Parameter(description = "The target id") @PathParam("id") String id) { LOG.debug("GraphSpace [{}] delete target: {}", graphSpace, id); + GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); try { - manager.authManager().deleteTarget(UserAPI.parseId(id)); + HugeTarget target = manager.authManager().getTarget( + graphSpace, UserAPI.parseId(id)); + checkGraphSpace(graphSpace, target); + manager.authManager().deleteTarget(graphSpace, + UserAPI.parseId(id)); } catch (NotFoundException e) { throw new IllegalArgumentException("Invalid target id: " + id); } } + static void checkGraphSpace(String graphSpace, HugeTarget target) { + if (!graphSpace.equals(target.graphSpace())) { + throw new jakarta.ws.rs.ForbiddenException( + "Permission denied: target belongs to another graphspace"); + } + } + @JsonIgnoreProperties(value = {"id", "target_creator", "target_create", "target_update"}) - private static class JsonTarget implements Checkable { + static class JsonTarget implements Checkable { @JsonProperty("target_name") @Schema(description = "The target name", required = true) @@ -160,6 +187,9 @@ private static class JsonTarget implements Checkable { @JsonProperty("target_url") @Schema(description = "The target URL", required = true) private String url; + @JsonProperty("target_description") + @Schema(description = "The target description") + private String description; @JsonProperty("target_resources") // error when List @Schema(description = "The target resources") private List> resources; @@ -177,11 +207,18 @@ public HugeTarget build(HugeTarget target) { if (this.resources != null) { target.resources(JsonUtil.toJson(this.resources)); } + if (this.description != null) { + target.description(this.description); + } return target; } - public HugeTarget build() { - HugeTarget target = new HugeTarget(this.name, this.graph, this.url); + public HugeTarget build(String graphSpace) { + String targetUrl = this.url == null ? "" : this.url; + HugeTarget target = new HugeTarget(this.name, this.graph, + targetUrl); + target.graphSpace(graphSpace); + target.description(this.description); if (this.resources != null) { target.resources(JsonUtil.toJson(this.resources)); } @@ -204,15 +241,13 @@ public void checkCreate(boolean isBatch) { "The name of target can't be null"); E.checkArgumentNotNull(this.graph, "The graph of target can't be null"); - E.checkArgumentNotNull(this.url, - "The url of target can't be null"); } @Override public void checkUpdate() { - E.checkArgument(this.url != null || - this.resources != null, - "Expect one of target url/resources"); + E.checkArgument(this.url != null || this.resources != null || + this.description != null, + "Expect one of target url/resources/description"); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java index 8b36dd9be4..18a2e3e71a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java @@ -61,48 +61,128 @@ public interface AuthManager { Id createTarget(HugeTarget target); + default Id createTarget(String graphSpace, HugeTarget target) { + return this.createTarget(target); + } + Id updateTarget(HugeTarget target); + default Id updateTarget(String graphSpace, HugeTarget target) { + return this.updateTarget(target); + } + HugeTarget deleteTarget(Id id); + default HugeTarget deleteTarget(String graphSpace, Id id) { + return this.deleteTarget(id); + } + HugeTarget getTarget(Id id); + default HugeTarget getTarget(String graphSpace, Id id) { + return this.getTarget(id); + } + List listTargets(List ids); List listAllTargets(long limit); + default List listAllTargets(String graphSpace, long limit) { + return this.listAllTargets(limit); + } + Id createBelong(HugeBelong belong); + default Id createBelong(String graphSpace, HugeBelong belong) { + return this.createBelong(belong); + } + Id updateBelong(HugeBelong belong); + default Id updateBelong(String graphSpace, HugeBelong belong) { + return this.updateBelong(belong); + } + HugeBelong deleteBelong(Id id); + default HugeBelong deleteBelong(String graphSpace, Id id) { + return this.deleteBelong(id); + } + HugeBelong getBelong(Id id); + default HugeBelong getBelong(String graphSpace, Id id) { + return this.getBelong(id); + } + List listBelong(List ids); List listAllBelong(long limit); + default List listAllBelong(String graphSpace, long limit) { + return this.listAllBelong(limit); + } + List listBelongByUser(Id user, long limit); + default List listBelongByUser(String graphSpace, Id user, + long limit) { + return this.listBelongByUser(user, limit); + } + List listBelongByGroup(Id group, long limit); + default List listBelongByGroup(String graphSpace, Id group, + long limit) { + return this.listBelongByGroup(group, limit); + } + Id createAccess(HugeAccess access); + default Id createAccess(String graphSpace, HugeAccess access) { + return this.createAccess(access); + } + Id updateAccess(HugeAccess access); + default Id updateAccess(String graphSpace, HugeAccess access) { + return this.updateAccess(access); + } + HugeAccess deleteAccess(Id id); + default HugeAccess deleteAccess(String graphSpace, Id id) { + return this.deleteAccess(id); + } + HugeAccess getAccess(Id id); + default HugeAccess getAccess(String graphSpace, Id id) { + return this.getAccess(id); + } + List listAccess(List ids); List listAllAccess(long limit); + default List listAllAccess(String graphSpace, long limit) { + return this.listAllAccess(limit); + } + List listAccessByGroup(Id group, long limit); + default List listAccessByGroup(String graphSpace, Id group, + long limit) { + return this.listAccessByGroup(group, limit); + } + List listAccessByTarget(Id target, long limit); + default List listAccessByTarget(String graphSpace, Id target, + long limit) { + return this.listAccessByTarget(target, limit); + } + Id createProject(HugeProject project); HugeProject deleteProject(Id id); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java index 2f5315b2d8..d95188d6c3 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java @@ -104,6 +104,10 @@ public String graphSpace() { return this.graphSpace; } + public void graphSpace(String graphSpace) { + this.graphSpace = graphSpace; + } + public String graph() { return this.graph; } @@ -157,6 +161,9 @@ protected boolean property(String key, Object value) { return true; } switch (key) { + case P.GRAPHSPACE: + this.graphSpace = (String) value; + break; case P.NAME: this.name = (String) value; break; @@ -166,6 +173,9 @@ protected boolean property(String key, Object value) { case P.URL: this.url = (String) value; break; + case P.DESCRIPTION: + this.description = (String) value; + break; case P.RESS: if (value instanceof String) { this.resources = JsonUtil.fromJson( @@ -226,10 +236,15 @@ public Map asMap() { Map map = new HashMap<>(); + map.put(Hidden.unHide(P.GRAPHSPACE), this.graphSpace); map.put(Hidden.unHide(P.NAME), this.name); map.put(Hidden.unHide(P.GRAPH), this.graph); map.put(Hidden.unHide(P.URL), this.url); + if (this.description != null) { + map.put(Hidden.unHide(P.DESCRIPTION), this.description); + } + if (this.resources != null && this.resources != EMPTY) { map.put(Hidden.unHide(P.RESS), this.resources); } @@ -253,9 +268,11 @@ public static final class P { public static final String ID = T.id.getAccessor(); public static final String LABEL = T.label.getAccessor(); + public static final String GRAPHSPACE = "~graphspace"; public static final String NAME = "~target_name"; public static final String GRAPH = "~target_graph"; public static final String URL = "~target_url"; + public static final String DESCRIPTION = "~target_description"; public static final String RESS = "~target_resources"; public static String unhide(String key) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java index 30cb05c1f4..626d9359b1 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java @@ -462,6 +462,11 @@ public List listAllGroups(long limit) { @Override public Id createTarget(HugeTarget target) { + return this.createTarget(this.graphSpace, target); + } + + @Override + public Id createTarget(String graphSpace, HugeTarget target) { try { target.create(target.update()); updateCreator(target); @@ -476,6 +481,11 @@ public Id createTarget(HugeTarget target) { @Override public Id updateTarget(HugeTarget target) { + return this.updateTarget(this.graphSpace, target); + } + + @Override + public Id updateTarget(String graphSpace, HugeTarget target) { try { HugeTarget result = this.metaManager.updateTarget(graphSpace, target); this.invalidateUserCache(); @@ -488,10 +498,16 @@ public Id updateTarget(HugeTarget target) { @Override public HugeTarget deleteTarget(Id id) { + return this.deleteTarget(this.graphSpace, id); + } + + @Override + public HugeTarget deleteTarget(String graphSpace, Id id) { try { - List accesses = this.listAccessByTarget(id, -1); + List accesses = this.listAccessByTarget(graphSpace, id, + -1); for (HugeAccess access : accesses) { - this.deleteAccess(access.id()); + this.deleteAccess(graphSpace, access.id()); } HugeTarget target = this.metaManager.deleteTarget(graphSpace, id); this.invalidateUserCache(); @@ -510,6 +526,7 @@ public HugeTarget getTarget(Id id) { return getTarget(this.graphSpace, id); } + @Override public HugeTarget getTarget(String graphSpace, Id id) { try { return this.metaManager.getTarget(graphSpace, id); @@ -537,6 +554,11 @@ public List listTargets(List ids) { @Override public List listAllTargets(long limit) { + return this.listAllTargets(this.graphSpace, limit); + } + + @Override + public List listAllTargets(String graphSpace, long limit) { try { return this.metaManager.listAllTargets(graphSpace, limit); } catch (IOException e) { @@ -550,6 +572,11 @@ public List listAllTargets(long limit) { @Override public Id createBelong(HugeBelong belong) { + return this.createBelong(this.graphSpace, belong); + } + + @Override + public Id createBelong(String graphSpace, HugeBelong belong) { try { belong.create(belong.update()); updateCreator(belong); @@ -566,6 +593,11 @@ public Id createBelong(HugeBelong belong) { @Override public Id updateBelong(HugeBelong belong) { + return this.updateBelong(this.graphSpace, belong); + } + + @Override + public Id updateBelong(String graphSpace, HugeBelong belong) { try { HugeBelong result = this.metaManager.updateBelong(graphSpace, belong); this.invalidateUserCache(); @@ -584,6 +616,7 @@ public HugeBelong deleteBelong(Id id) { return this.deleteBelong(this.graphSpace, id); } + @Override public HugeBelong deleteBelong(String graphSpace, Id id) { try { HugeBelong result = this.metaManager.deleteBelong(graphSpace, id); @@ -600,6 +633,11 @@ public HugeBelong deleteBelong(String graphSpace, Id id) { @Override public HugeBelong getBelong(Id id) { + return this.getBelong(this.graphSpace, id); + } + + @Override + public HugeBelong getBelong(String graphSpace, Id id) { try { return this.metaManager.getBelong(graphSpace, id); } catch (IOException e) { @@ -626,6 +664,11 @@ public List listBelong(List ids) { @Override public List listAllBelong(long limit) { + return this.listAllBelong(this.graphSpace, limit); + } + + @Override + public List listAllBelong(String graphSpace, long limit) { try { return this.metaManager.listAllBelong(graphSpace, limit); } catch (IOException e) { @@ -639,8 +682,15 @@ public List listAllBelong(long limit) { @Override public List listBelongByUser(Id user, long limit) { + return this.listBelongByUser(this.graphSpace, user, limit); + } + + @Override + public List listBelongByUser(String graphSpace, Id user, + long limit) { try { - return this.metaManager.listBelongBySource(this.graphSpace, user, "*", limit); + return this.metaManager.listBelongBySource(graphSpace, user, "*", + limit); } catch (IOException e) { throw new HugeException("IOException occurs when " + "list belong by user", e); @@ -652,8 +702,15 @@ public List listBelongByUser(Id user, long limit) { @Override public List listBelongByGroup(Id role, long limit) { + return this.listBelongByGroup(this.graphSpace, role, limit); + } + + @Override + public List listBelongByGroup(String graphSpace, Id role, + long limit) { try { - return this.metaManager.listBelongByTarget(this.graphSpace, role, "*", limit); + return this.metaManager.listBelongByTarget(graphSpace, role, "*", + limit); } catch (IOException e) { throw new HugeException("IOException occurs when " + "list belong by user", e); @@ -665,6 +722,11 @@ public List listBelongByGroup(Id role, long limit) { @Override public Id createAccess(HugeAccess access) { + return this.createAccess(this.graphSpace, access); + } + + @Override + public Id createAccess(String graphSpace, HugeAccess access) { try { access.create(access.update()); updateCreator(access); @@ -682,6 +744,11 @@ public Id createAccess(HugeAccess access) { @Override public Id updateAccess(HugeAccess access) { + return this.updateAccess(this.graphSpace, access); + } + + @Override + public Id updateAccess(String graphSpace, HugeAccess access) { HugeAccess result = null; try { result = this.metaManager.updateAccess(graphSpace, access); @@ -698,7 +765,11 @@ public Id updateAccess(HugeAccess access) { @Override public HugeAccess deleteAccess(Id id) { + return this.deleteAccess(this.graphSpace, id); + } + @Override + public HugeAccess deleteAccess(String graphSpace, Id id) { try { HugeAccess result = this.metaManager.deleteAccess(graphSpace, id); this.invalidateUserCache(); @@ -714,6 +785,11 @@ public HugeAccess deleteAccess(Id id) { @Override public HugeAccess getAccess(Id id) { + return this.getAccess(this.graphSpace, id); + } + + @Override + public HugeAccess getAccess(String graphSpace, Id id) { try { return this.metaManager.getAccess(graphSpace, id); } catch (IOException e) { @@ -740,6 +816,11 @@ public List listAccess(List ids) { @Override public List listAllAccess(long limit) { + return this.listAllAccess(this.graphSpace, limit); + } + + @Override + public List listAllAccess(String graphSpace, long limit) { try { return this.metaManager.listAllAccess(graphSpace, limit); } catch (IOException e) { @@ -753,6 +834,12 @@ public List listAllAccess(long limit) { @Override public List listAccessByGroup(Id group, long limit) { + return this.listAccessByGroup(this.graphSpace, group, limit); + } + + @Override + public List listAccessByGroup(String graphSpace, Id group, + long limit) { try { return this.metaManager.listAccessByGroup(graphSpace, group, limit); } catch (IOException e) { @@ -766,8 +853,15 @@ public List listAccessByGroup(Id group, long limit) { @Override public List listAccessByTarget(Id target, long limit) { + return this.listAccessByTarget(this.graphSpace, target, limit); + } + + @Override + public List listAccessByTarget(String graphSpace, Id target, + long limit) { try { - return this.metaManager.listAccessByTarget(this.graphSpace, target, limit); + return this.metaManager.listAccessByTarget(graphSpace, target, + limit); } catch (IOException e) { throw new HugeException("IOException occurs when " + "get access list by target", e); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java new file mode 100644 index 0000000000..06c89f9187 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.api.auth; + +import java.util.Map; + +import org.apache.hugegraph.auth.HugeAccess; +import org.apache.hugegraph.auth.HugeBelong; +import org.apache.hugegraph.auth.HugePermission; +import org.apache.hugegraph.auth.HugeTarget; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.ws.rs.ForbiddenException; + +public class GraphSpaceAuthPayloadTest { + + @Test + public void testTargetPayloadUsesPathGraphSpaceAndOptionalUrl() + throws Exception { + TargetAPI.JsonTarget jsonTarget = new ObjectMapper().readValue( + "{\"target_name\":\"target\"," + + "\"target_graph\":\"hugegraph\"," + + "\"target_description\":\"description\"," + + "\"target_resources\":[]}", + TargetAPI.JsonTarget.class); + + HugeTarget target = jsonTarget.build("SPACE_A"); + target.creator("manager"); + Map properties = target.asMap(); + + Assert.assertEquals("SPACE_A", target.graphSpace()); + Assert.assertEquals("description", target.description()); + Assert.assertEquals("", target.url()); + Assert.assertEquals("SPACE_A", properties.get("graphspace")); + Assert.assertEquals("description", + properties.get("target_description")); + } + + @Test + public void testRelationshipPayloadsUsePathGraphSpace() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + AccessAPI.JsonAccess jsonAccess = mapper.readValue( + "{\"group\":\"group\",\"target\":\"target\"," + + "\"access_permission\":\"READ\"}", + AccessAPI.JsonAccess.class); + HugeAccess access = jsonAccess.build("SPACE_A"); + + Assert.assertEquals("SPACE_A", access.graphSpace()); + Assert.assertEquals(HugePermission.READ, access.permission()); + + BelongAPI.JsonBelong jsonBelong = mapper.readValue( + "{\"user\":\"user\",\"group\":\"group\"}", + BelongAPI.JsonBelong.class); + HugeBelong belong = jsonBelong.build("SPACE_A"); + + Assert.assertEquals("SPACE_A", belong.graphSpace()); + Assert.assertEquals(HugeBelong.UG, belong.link()); + } + + @Test + public void testTargetRejectsForeignGraphSpace() { + HugeTarget target = new HugeTarget("target", "hugegraph", ""); + target.graphSpace("SPACE_A"); + + TargetAPI.checkGraphSpace("SPACE_A", target); + Assert.assertThrows(ForbiddenException.class, () -> + TargetAPI.checkGraphSpace("SPACE_B", target)); + } + + @Test + public void testAccessRejectsForeignGraphSpace() { + HugeAccess access = new HugeAccess("SPACE_A", + IdGenerator.of("group"), + IdGenerator.of("target")); + + AccessAPI.checkGraphSpace("SPACE_A", access); + Assert.assertThrows(ForbiddenException.class, () -> + AccessAPI.checkGraphSpace("SPACE_B", access)); + } + + @Test + public void testBelongRejectsForeignGraphSpace() { + HugeBelong belong = new HugeBelong("SPACE_A", + IdGenerator.of("user"), + IdGenerator.of("group"), + null, HugeBelong.UG); + + BelongAPI.checkGraphSpace("SPACE_A", belong); + Assert.assertThrows(ForbiddenException.class, () -> + BelongAPI.checkGraphSpace("SPACE_B", belong)); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java new file mode 100644 index 0000000000..3fa8b94fe1 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.api.auth; + +import java.util.Arrays; +import java.util.List; + +import org.apache.hugegraph.auth.AuthManager; +import org.apache.hugegraph.auth.HugeGroup; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import jakarta.ws.rs.ForbiddenException; + +public class GraphSpaceGroupAPITest { + + @Test + public void testSpaceManagerCanOnlyUseOwnedGraphSpaceEndpoint() { + AuthManager auth = Mockito.mock(AuthManager.class); + Mockito.when(auth.isAdminManager("manager")).thenReturn(false); + Mockito.when(auth.isSpaceManager("SPACE_A", "manager")) + .thenReturn(true); + + GraphSpaceGroupAPI.checkManagerPermission(auth, "SPACE_A", + "manager"); + Assert.assertThrows(ForbiddenException.class, () -> { + GraphSpaceGroupAPI.checkManagerPermission(auth, "SPACE_B", + "manager"); + }); + Assert.assertThrows(ForbiddenException.class, () -> { + GraphSpaceGroupAPI.checkManagerPermission(auth, "SPACE_A", + "ordinary"); + }); + } + + @Test + public void testScopedGroupFilterNeverReturnsForeignOrLegacyGroup() { + HugeGroup own = group("SPACE_A", 'a'); + HugeGroup foreign = group("SPACE_B", 'b'); + HugeGroup legacy = new HugeGroup("legacy-role"); + + List filtered = GraphSpaceGroupAPI.filterScopedGroups( + "SPACE_A", + Arrays.asList(own, foreign, legacy)); + + Assert.assertEquals(1, filtered.size()); + Assert.assertSame(own, filtered.get(0)); + } + + @Test + public void testScopedGroupValidationRejectsForeignAndLegacyGroup() { + HugeGroup own = group("SPACE_A", 'a'); + HugeGroup foreign = group("SPACE_B", 'b'); + HugeGroup legacy = new HugeGroup("legacy-role"); + + GraphSpaceGroupAPI.checkScopedGroup("SPACE_A", own); + Assert.assertThrows(ForbiddenException.class, () -> { + GraphSpaceGroupAPI.checkScopedGroup("SPACE_A", foreign); + }); + Assert.assertThrows(ForbiddenException.class, () -> { + GraphSpaceGroupAPI.checkScopedGroup("SPACE_A", legacy); + }); + } + + private static HugeGroup group(String graphSpace, char suffix) { + return new HugeGroup(GraphSpaceGroupAPI.scopedPrefix(graphSpace) + + repeat(suffix, 32)); + } + + private static String repeat(char value, int count) { + StringBuilder builder = new StringBuilder(count); + for (int i = 0; i < count; i++) { + builder.append(value); + } + return builder.toString(); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index 68fa646ec9..3ce9f8b9f1 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -18,6 +18,8 @@ package org.apache.hugegraph.unit; import org.apache.hugegraph.auth.WsAndHttpBasicAuthHandlerTest; +import org.apache.hugegraph.api.auth.GraphSpaceGroupAPITest; +import org.apache.hugegraph.api.auth.GraphSpaceAuthPayloadTest; import org.apache.hugegraph.core.RoleElectionStateMachineTest; import org.apache.hugegraph.meta.EtcdMetaDriverTest; import org.apache.hugegraph.meta.MetaManagerSchemaCacheClearEventTest; @@ -98,6 +100,8 @@ /* api gremlin */ GremlinQueryAPITest.class, WsAndHttpBasicAuthHandlerTest.class, + GraphSpaceGroupAPITest.class, + GraphSpaceAuthPayloadTest.class, /* api space */ GraphSpaceAPITest.class, From 9e650ff41e065813944b2680dd7044454040a550 Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 15 Jul 2026 07:38:48 +0800 Subject: [PATCH 07/15] fix(server): parse HTTP Basic credentials correctly --- .../auth/WsAndHttpBasicAuthHandler.java | 6 +-- .../auth/WsAndHttpBasicAuthHandlerTest.java | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java index 3c3dd71b59..821c90de46 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java @@ -90,7 +90,7 @@ public static boolean isWebSocket(final HttpMessage msg) { private static class HttpBasicAuthHandler extends AbstractAuthenticationHandler { - private final Base64.Decoder decoder = Base64.getUrlDecoder(); + private final Base64.Decoder decoder = Base64.getDecoder(); public HttpBasicAuthHandler(Authenticator authenticator) { super(authenticator); @@ -146,11 +146,11 @@ private boolean parseBasicCredentials(String header, try { String encoded = header.substring(BASIC_AUTH_PREFIX.length()); userPass = this.decoder.decode(encoded); - } catch (IndexOutOfBoundsException | IllegalArgumentException e) { + } catch (IllegalArgumentException e) { return false; } String authorization = new String(userPass, StandardCharsets.UTF_8); - String[] split = authorization.split(":"); + String[] split = authorization.split(":", 2); if (split.length != 2) { return false; } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java index 716adbcabd..0bd8c20e07 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java @@ -94,6 +94,51 @@ public void testBasicCredentialsStillAuthenticateHttpGremlinRequest() channel.finishAndReleaseAll(); } + @SuppressWarnings("unchecked") + @Test + public void testBasicPasswordContainingColonAuthenticatesHttpRequest() + throws Exception { + Authenticator authenticator = Mockito.mock(Authenticator.class); + Mockito.when(authenticator.authenticate(Mockito.anyMap())) + .thenReturn(new AuthenticatedUser("admin")); + EmbeddedChannel channel = channel(authenticator); + String encoded = Base64.getEncoder().encodeToString( + "admin:p:assword".getBytes(StandardCharsets.UTF_8)); + + channel.writeInbound(request("Basic " + encoded)); + + ArgumentCaptor> credentials = + ArgumentCaptor.forClass(Map.class); + Mockito.verify(authenticator).authenticate(credentials.capture()); + Assert.assertEquals("admin", credentials.getValue().get("username")); + Assert.assertEquals("p:assword", + credentials.getValue().get("password")); + channel.finishAndReleaseAll(); + } + + @SuppressWarnings("unchecked") + @Test + public void testBasicCredentialsUseStandardBase64Decoder() + throws Exception { + Authenticator authenticator = Mockito.mock(Authenticator.class); + Mockito.when(authenticator.authenticate(Mockito.anyMap())) + .thenReturn(new AuthenticatedUser("admin")); + EmbeddedChannel channel = channel(authenticator); + String password = "\uFF7F"; + String encoded = Base64.getEncoder().encodeToString( + ("admin:" + password).getBytes(StandardCharsets.UTF_8)); + Assert.assertTrue(encoded.contains("/")); + + channel.writeInbound(request("Basic " + encoded)); + + ArgumentCaptor> credentials = + ArgumentCaptor.forClass(Map.class); + Mockito.verify(authenticator).authenticate(credentials.capture()); + Assert.assertEquals(password, + credentials.getValue().get("password")); + channel.finishAndReleaseAll(); + } + @Test public void testEmptyBearerTokenIsRejected() throws Exception { assertUnauthorized("Bearer "); From eb0a59e1a7b7e0e1feb7187931d604c3ee10f0bf Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 15 Jul 2026 08:01:01 +0800 Subject: [PATCH 08/15] test(server): align scoped target metadata --- .../src/main/java/org/apache/hugegraph/core/AuthTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java index a6da50310c..7411b89397 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java @@ -545,7 +545,8 @@ public void testCreateTarget() { "target_creator", "admin")); expected.putAll(ImmutableMap.of("target_create", target.create(), "target_update", target.update(), - "id", target.id())); + "id", target.id(), + "graphspace", "DEFAULT")); Assert.assertEquals(expected, target.asMap()); } From 91f6cef0bca7856259f60555676b91f35886bb10 Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 15 Jul 2026 08:57:24 +0800 Subject: [PATCH 09/15] fix(auth): enforce scoped metadata contracts --- .../api/auth/GraphSpaceGroupAPI.java | 25 ++- .../org/apache/hugegraph/auth/HugeTarget.java | 44 +++- .../hugegraph/auth/StandardAuthManager.java | 145 +++++++++++++ .../api/auth/GraphSpaceGroupAPITest.java | 47 +++++ .../org/apache/hugegraph/core/AuthTest.java | 198 ++++++++++++++++++ 5 files changed, 453 insertions(+), 6 deletions(-) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java index 4ee26be15c..b9e54218e7 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.List; +import java.util.UUID; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; @@ -78,7 +79,7 @@ public String create(@Context GraphManager manager, LOG.debug("GraphSpace [{}] create scoped group", graphSpace); ensureManager(manager, graphSpace); checkCreatingBody(jsonGroup); - HugeGroup group = jsonGroup.build(); + HugeGroup group = jsonGroup.build(graphSpace); checkScopedGroup(graphSpace, group); group.id(manager.authManager().createGroup(group)); return manager.serializer().writeAuthElement(group); @@ -111,11 +112,23 @@ public String list(@Context GraphManager manager, @QueryParam("limit") @DefaultValue("100") long limit) { LOG.debug("GraphSpace [{}] list scoped groups", graphSpace); ensureManager(manager, graphSpace); - List groups = manager.authManager().listAllGroups(limit); - groups = filterScopedGroups(graphSpace, groups); + List groups = listScopedGroups(manager.authManager(), + graphSpace, limit); return manager.serializer().writeAuthElements("groups", groups); } + static List listScopedGroups(AuthManager authManager, + String graphSpace, long limit) { + E.checkArgument(limit >= -1L, + "The limit must be -1 or a non-negative number"); + List groups = authManager.listAllGroups(-1); + groups = filterScopedGroups(graphSpace, groups); + if (limit >= 0L && groups.size() > limit) { + return new ArrayList<>(groups.subList(0, (int) limit)); + } + return groups; + } + @GET @Timed @Path("{id}") @@ -268,8 +281,10 @@ HugeGroup build(HugeGroup group) { return group; } - HugeGroup build() { - HugeGroup group = new HugeGroup(this.name); + HugeGroup build(String graphSpace) { + String name = scopedPrefix(graphSpace) + + UUID.randomUUID().toString().replace("-", ""); + HugeGroup group = new HugeGroup(name); group.description(this.description); return group; } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java index d95188d6c3..446bfd2445 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java @@ -218,6 +218,16 @@ protected Object[] asArray() { list.add(P.URL); list.add(this.url); + if (this.graphSpace != null) { + list.add(P.GRAPHSPACE); + list.add(this.graphSpace); + } + + if (this.description != null) { + list.add(P.DESCRIPTION); + list.add(this.description); + } + if (!this.isResourceEmpty()) { list.add(P.RESS); list.add(JsonUtil.toJson(this.resources)); @@ -293,6 +303,7 @@ public Schema(HugeGraphParams graph) { @Override public void initSchemaIfNeeded() { if (this.existVertexLabel(this.label)) { + this.upgradeSchemaIfNeeded(); return; } @@ -303,18 +314,49 @@ public void initSchemaIfNeeded() { .properties(properties) .usePrimaryKeyId() .primaryKeys(P.NAME) - .nullableKeys(P.RESS) + .nullableKeys(P.GRAPHSPACE, P.DESCRIPTION, + P.RESS) .enableLabelIndex(true) .build(); this.graph.schemaTransaction().addVertexLabel(label); } + private void upgradeSchemaIfNeeded() { + VertexLabel label = this.graph.graph().vertexLabel(this.label); + boolean changed = this.ensureNullableProperty(label, + P.GRAPHSPACE); + changed |= this.ensureNullableProperty(label, P.DESCRIPTION); + if (changed) { + this.graph.schemaTransaction().updateVertexLabel(label); + } + } + + private boolean ensureNullableProperty(VertexLabel label, + String property) { + if (!this.graph.graph().existsPropertyKey(property)) { + createPropertyKey(property); + } + Id propertyId = this.graph.graph().propertyKey(property).id(); + boolean changed = false; + if (!label.properties().contains(propertyId)) { + label.property(propertyId); + changed = true; + } + if (!label.nullableKeys().contains(propertyId)) { + label.nullableKey(propertyId); + changed = true; + } + return changed; + } + private String[] initProperties() { List props = new ArrayList<>(); props.add(createPropertyKey(P.NAME)); props.add(createPropertyKey(P.GRAPH)); props.add(createPropertyKey(P.URL)); + props.add(createPropertyKey(P.GRAPHSPACE)); + props.add(createPropertyKey(P.DESCRIPTION)); props.add(createPropertyKey(P.RESS)); return super.initProperties(props); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java index cac9ba53dd..99e00dd0ac 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java @@ -164,6 +164,13 @@ private void invalidatePasswordCache(Id id) { this.tokenCache.clear(); } + private void checkGraphSpace(String graphSpace) { + E.checkArgument(this.defaultGraphSpace.equals(graphSpace), + "The standalone auth manager only supports graph " + + "space '%s', but got '%s'", + this.defaultGraphSpace, graphSpace); + } + @Override public Id createUser(HugeUser user) { this.invalidateUserCache(); @@ -255,23 +262,52 @@ public Id createTarget(HugeTarget target) { return this.targets.add(target); } + @Override + public Id createTarget(String graphSpace, HugeTarget target) { + this.checkGraphSpace(graphSpace); + this.checkGraphSpace(target.graphSpace()); + return this.createTarget(target); + } + @Override public Id updateTarget(HugeTarget target) { this.invalidateUserCache(); return this.targets.update(target); } + @Override + public Id updateTarget(String graphSpace, HugeTarget target) { + this.checkGraphSpace(graphSpace); + this.checkGraphSpace(target.graphSpace()); + return this.updateTarget(target); + } + @Override public HugeTarget deleteTarget(Id id) { this.invalidateUserCache(); return this.targets.delete(id); } + @Override + public HugeTarget deleteTarget(String graphSpace, Id id) { + this.checkGraphSpace(graphSpace); + this.checkGraphSpace(this.getTarget(id).graphSpace()); + return this.deleteTarget(id); + } + @Override public HugeTarget getTarget(Id id) { return this.targets.get(id); } + @Override + public HugeTarget getTarget(String graphSpace, Id id) { + this.checkGraphSpace(graphSpace); + HugeTarget target = this.getTarget(id); + this.checkGraphSpace(target.graphSpace()); + return target; + } + @Override public List listTargets(List ids) { return this.targets.list(ids); @@ -282,6 +318,23 @@ public List listAllTargets(long limit) { return this.targets.list(limit); } + @Override + public List listAllTargets(String graphSpace, long limit) { + this.checkGraphSpace(graphSpace); + E.checkArgument(limit >= -1L, + "The limit must be -1 or a non-negative number"); + List targets = new ArrayList<>(); + for (HugeTarget target : this.listAllTargets(-1)) { + if (this.defaultGraphSpace.equals(target.graphSpace())) { + targets.add(target); + } + } + if (limit >= 0L && targets.size() > limit) { + return new ArrayList<>(targets.subList(0, (int) limit)); + } + return targets; + } + @Override public Id createBelong(HugeBelong belong) { this.invalidateUserCache(); @@ -292,23 +345,49 @@ public Id createBelong(HugeBelong belong) { return this.belong.add(belong); } + @Override + public Id createBelong(String graphSpace, HugeBelong belong) { + this.checkGraphSpace(graphSpace); + this.checkGraphSpace(belong.graphSpace()); + return this.createBelong(belong); + } + @Override public Id updateBelong(HugeBelong belong) { this.invalidateUserCache(); return this.belong.update(belong); } + @Override + public Id updateBelong(String graphSpace, HugeBelong belong) { + this.checkGraphSpace(graphSpace); + this.checkGraphSpace(belong.graphSpace()); + return this.updateBelong(belong); + } + @Override public HugeBelong deleteBelong(Id id) { this.invalidateUserCache(); return this.belong.delete(id); } + @Override + public HugeBelong deleteBelong(String graphSpace, Id id) { + this.checkGraphSpace(graphSpace); + return this.deleteBelong(id); + } + @Override public HugeBelong getBelong(Id id) { return this.belong.get(id); } + @Override + public HugeBelong getBelong(String graphSpace, Id id) { + this.checkGraphSpace(graphSpace); + return this.getBelong(id); + } + @Override public List listBelong(List ids) { return this.belong.list(ids); @@ -319,18 +398,38 @@ public List listAllBelong(long limit) { return this.belong.list(limit); } + @Override + public List listAllBelong(String graphSpace, long limit) { + this.checkGraphSpace(graphSpace); + return this.listAllBelong(limit); + } + @Override public List listBelongByUser(Id user, long limit) { return this.belong.list(user, Directions.OUT, HugeBelong.P.BELONG, limit); } + @Override + public List listBelongByUser(String graphSpace, Id user, + long limit) { + this.checkGraphSpace(graphSpace); + return this.listBelongByUser(user, limit); + } + @Override public List listBelongByGroup(Id group, long limit) { return this.belong.list(group, Directions.IN, HugeBelong.P.BELONG, limit); } + @Override + public List listBelongByGroup(String graphSpace, Id group, + long limit) { + this.checkGraphSpace(graphSpace); + return this.listBelongByGroup(group, limit); + } + @Override public Id createAccess(HugeAccess access) { this.invalidateUserCache(); @@ -341,23 +440,49 @@ public Id createAccess(HugeAccess access) { return this.access.add(access); } + @Override + public Id createAccess(String graphSpace, HugeAccess access) { + this.checkGraphSpace(graphSpace); + this.checkGraphSpace(access.graphSpace()); + return this.createAccess(access); + } + @Override public Id updateAccess(HugeAccess access) { this.invalidateUserCache(); return this.access.update(access); } + @Override + public Id updateAccess(String graphSpace, HugeAccess access) { + this.checkGraphSpace(graphSpace); + this.checkGraphSpace(access.graphSpace()); + return this.updateAccess(access); + } + @Override public HugeAccess deleteAccess(Id id) { this.invalidateUserCache(); return this.access.delete(id); } + @Override + public HugeAccess deleteAccess(String graphSpace, Id id) { + this.checkGraphSpace(graphSpace); + return this.deleteAccess(id); + } + @Override public HugeAccess getAccess(Id id) { return this.access.get(id); } + @Override + public HugeAccess getAccess(String graphSpace, Id id) { + this.checkGraphSpace(graphSpace); + return this.getAccess(id); + } + @Override public List listAccess(List ids) { return this.access.list(ids); @@ -368,18 +493,38 @@ public List listAllAccess(long limit) { return this.access.list(limit); } + @Override + public List listAllAccess(String graphSpace, long limit) { + this.checkGraphSpace(graphSpace); + return this.listAllAccess(limit); + } + @Override public List listAccessByGroup(Id group, long limit) { return this.access.list(group, Directions.OUT, HugeAccess.P.ACCESS, limit); } + @Override + public List listAccessByGroup(String graphSpace, Id group, + long limit) { + this.checkGraphSpace(graphSpace); + return this.listAccessByGroup(group, limit); + } + @Override public List listAccessByTarget(Id target, long limit) { return this.access.list(target, Directions.IN, HugeAccess.P.ACCESS, limit); } + @Override + public List listAccessByTarget(String graphSpace, Id target, + long limit) { + this.checkGraphSpace(graphSpace); + return this.listAccessByTarget(target, limit); + } + @Override public Id createProject(HugeProject project) { E.checkArgument(!StringUtils.isEmpty(project.name()), diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java index 3fa8b94fe1..a3b2475638 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.api.auth; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -26,10 +27,56 @@ import org.junit.Test; import org.mockito.Mockito; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.ws.rs.ForbiddenException; public class GraphSpaceGroupAPITest { + @Test + public void testCreatePayloadCannotControlScopedGroupName() + throws Exception { + GraphSpaceGroupAPI.JsonGroup jsonGroup = new ObjectMapper().readValue( + "{\"group_name\":\"client-controlled\"," + + "\"group_description\":\"description\"}", + GraphSpaceGroupAPI.JsonGroup.class); + + HugeGroup group = jsonGroup.build("SPACE_A"); + + Assert.assertTrue(group.name().matches( + GraphSpaceGroupAPI.scopedPrefix("SPACE_A") + + "[0-9a-f]{32}")); + Assert.assertNotEquals("client-controlled", group.name()); + Assert.assertEquals("description", group.description()); + } + + @Test + public void testListFiltersBeforeApplyingLimit() { + AuthManager auth = Mockito.mock(AuthManager.class); + + List groups = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + groups.add(group("SPACE_B", (char) ('a' + i % 6))); + } + groups.add(group("SPACE_A", 'a')); + groups.add(group("SPACE_A", 'b')); + Mockito.when(auth.listAllGroups(Mockito.anyLong())).thenReturn(groups); + + List one = GraphSpaceGroupAPI.listScopedGroups( + auth, "SPACE_A", 1); + List hundred = GraphSpaceGroupAPI.listScopedGroups( + auth, "SPACE_A", 100); + List unlimited = GraphSpaceGroupAPI.listScopedGroups( + auth, "SPACE_A", -1); + + Assert.assertEquals(1, one.size()); + Assert.assertEquals(2, hundred.size()); + Assert.assertEquals(2, unlimited.size()); + Mockito.verify(auth, Mockito.times(3)).listAllGroups(-1); + Assert.assertThrows(IllegalArgumentException.class, () -> { + GraphSpaceGroupAPI.listScopedGroups(auth, "SPACE_A", -2); + }); + } + @Test public void testSpaceManagerCanOnlyUseOwnedGraphSpaceEndpoint() { AuthManager auth = Mockito.mock(AuthManager.class); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java index 7411b89397..e4fc90455b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java @@ -29,6 +29,7 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.hugegraph.HugeException; import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeAccess; import org.apache.hugegraph.auth.HugeBelong; @@ -39,10 +40,13 @@ import org.apache.hugegraph.auth.HugeTarget; import org.apache.hugegraph.auth.HugeUser; import org.apache.hugegraph.auth.RolePermission; +import org.apache.hugegraph.auth.SchemaDefine; +import org.apache.hugegraph.auth.StandardAuthManager; import org.apache.hugegraph.auth.UserWithRole; import org.apache.hugegraph.backend.cache.Cache; import org.apache.hugegraph.backend.id.Id; import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.schema.VertexLabel; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.util.JsonUtil; @@ -536,6 +540,8 @@ public void testCreateTarget() { target = authManager.getTarget(id); Assert.assertEquals("graph1", target.name()); Assert.assertEquals("127.0.0.1:8080", target.url()); + Assert.assertEquals("DEFAULT", target.graphSpace()); + Assert.assertNull(target.description()); Assert.assertEquals(target.create(), target.update()); HashMap expected = new HashMap<>(); @@ -551,6 +557,198 @@ public void testCreateTarget() { Assert.assertEquals(expected, target.asMap()); } + @Test + public void testTargetScopedRoundTripAfterAuthManagerReopen() { + AuthManager authManager = graph().authManager(); + Assume.assumeTrue(authManager instanceof StandardAuthManager); + + HugeTarget target = makeTarget("target-scoped", "url"); + target.graphSpace("DEFAULT"); + target.description("description"); + Id id = authManager.createTarget("DEFAULT", target); + + StandardAuthManager reopened = new StandardAuthManager(params()); + reopened.init(); + HugeTarget stored = reopened.getTarget("DEFAULT", id); + + Assert.assertEquals("DEFAULT", stored.graphSpace()); + Assert.assertEquals("description", stored.description()); + reopened.close(); + } + + @Test + public void testTargetSchemaUpgradeAddsNullableScopedProperties() { + LegacyTargetSchema schema = new LegacyTargetSchema(params()); + schema.initSchemaIfNeeded(); + VertexLabel legacy = graph().vertexLabel(HugeTarget.P.TARGET); + List legacyProperties = graph().mapPkId2Name( + legacy.properties()); + Assert.assertFalse(legacyProperties.contains(HugeTarget.P.GRAPHSPACE)); + Assert.assertFalse(legacyProperties.contains( + HugeTarget.P.DESCRIPTION)); + + HugeTarget.schema(params()).initSchemaIfNeeded(); + + VertexLabel upgraded = graph().vertexLabel(HugeTarget.P.TARGET); + Id graphSpace = graph().propertyKey(HugeTarget.P.GRAPHSPACE).id(); + Id description = graph().propertyKey(HugeTarget.P.DESCRIPTION).id(); + Assert.assertTrue(upgraded.properties().contains(graphSpace)); + Assert.assertTrue(upgraded.properties().contains(description)); + Assert.assertTrue(upgraded.nullableKeys().contains(graphSpace)); + Assert.assertTrue(upgraded.nullableKeys().contains(description)); + } + + @Test + public void testStandaloneScopedOverloadsRejectForeignGraphSpace() { + AuthManager authManager = graph().authManager(); + Assume.assumeTrue(authManager instanceof StandardAuthManager); + authManager.init(); + + Id fake = IdGenerator.of("fake"); + HugeTarget target = makeTarget("foreign-target", "url"); + HugeBelong belong = makeBelong(fake, fake); + HugeAccess access = makeAccess(fake, fake, HugePermission.READ); + List operations = ImmutableList.of( + () -> authManager.createTarget("FOREIGN", target), + () -> authManager.updateTarget("FOREIGN", target), + () -> authManager.deleteTarget("FOREIGN", fake), + () -> authManager.getTarget("FOREIGN", fake), + () -> authManager.listAllTargets("FOREIGN", -1), + () -> authManager.createBelong("FOREIGN", belong), + () -> authManager.updateBelong("FOREIGN", belong), + () -> authManager.deleteBelong("FOREIGN", fake), + () -> authManager.getBelong("FOREIGN", fake), + () -> authManager.listAllBelong("FOREIGN", -1), + () -> authManager.listBelongByUser("FOREIGN", fake, -1), + () -> authManager.listBelongByGroup("FOREIGN", fake, -1), + () -> authManager.createAccess("FOREIGN", access), + () -> authManager.updateAccess("FOREIGN", access), + () -> authManager.deleteAccess("FOREIGN", fake), + () -> authManager.getAccess("FOREIGN", fake), + () -> authManager.listAllAccess("FOREIGN", -1), + () -> authManager.listAccessByGroup("FOREIGN", fake, -1), + () -> authManager.listAccessByTarget("FOREIGN", fake, -1)); + + for (Runnable operation : operations) { + Assert.assertThrows(IllegalArgumentException.class, + operation::run, + e -> Assert.assertContains("DEFAULT", + e.getMessage())); + } + Assert.assertEquals(0, authManager.listAllTargets(-1).size()); + Assert.assertEquals(0, authManager.listAllBelong(-1).size()); + Assert.assertEquals(0, authManager.listAllAccess(-1).size()); + } + + @Test + public void testStandaloneScopedWritesRejectForeignEntityGraphSpace() { + AuthManager authManager = graph().authManager(); + Assume.assumeTrue(authManager instanceof StandardAuthManager); + authManager.init(); + + Id fake = IdGenerator.of("fake"); + HugeTarget target = makeTarget("foreign-target", "url"); + target.graphSpace("FOREIGN"); + HugeBelong belong = new HugeBelong("FOREIGN", fake, fake, + null, HugeBelong.UG); + HugeAccess access = new HugeAccess("FOREIGN", fake, fake, + HugePermission.READ); + List operations = ImmutableList.of( + () -> authManager.createTarget("DEFAULT", target), + () -> authManager.updateTarget("DEFAULT", target), + () -> authManager.createBelong("DEFAULT", belong), + () -> authManager.updateBelong("DEFAULT", belong), + () -> authManager.createAccess("DEFAULT", access), + () -> authManager.updateAccess("DEFAULT", access)); + + for (Runnable operation : operations) { + Assert.assertThrows(IllegalArgumentException.class, + operation::run, + e -> Assert.assertContains("DEFAULT", + e.getMessage())); + } + Assert.assertEquals(0, authManager.listAllTargets(-1).size()); + Assert.assertEquals(0, authManager.listAllBelong(-1).size()); + Assert.assertEquals(0, authManager.listAllAccess(-1).size()); + } + + @Test + public void testStandaloneScopedOverloadsPreserveDefaultCompatibility() { + AuthManager authManager = graph().authManager(); + Assume.assumeTrue(authManager instanceof StandardAuthManager); + authManager.init(); + + Id user = authManager.createUser(makeUser("default-user", "pass")); + Id group = authManager.createGroup(makeGroup("default-group")); + HugeTarget target = makeTarget("default-target", "url"); + Id targetId = authManager.createTarget("DEFAULT", target); + HugeBelong belong = makeBelong(user, group); + Id belongId = authManager.createBelong("DEFAULT", belong); + HugeAccess access = makeAccess(group, targetId, HugePermission.READ); + Id accessId = authManager.createAccess("DEFAULT", access); + + Assert.assertEquals(targetId, + authManager.getTarget("DEFAULT", targetId).id()); + Assert.assertEquals(belongId, + authManager.getBelong("DEFAULT", belongId).id()); + Assert.assertEquals(accessId, + authManager.getAccess("DEFAULT", accessId).id()); + Assert.assertEquals(1, + authManager.listAllTargets("DEFAULT", -1).size()); + Assert.assertEquals(1, + authManager.listAllBelong("DEFAULT", -1).size()); + Assert.assertEquals(1, + authManager.listAllAccess("DEFAULT", -1).size()); + } + + @Test + public void testStandaloneScopedReadsHidePreexistingForeignTarget() { + AuthManager authManager = graph().authManager(); + Assume.assumeTrue(authManager instanceof StandardAuthManager); + authManager.init(); + + HugeTarget target = makeTarget("legacy-foreign-target", "url"); + target.graphSpace("FOREIGN"); + Id id = authManager.createTarget(target); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + authManager.getTarget("DEFAULT", id); + }); + Assert.assertEquals(0, + authManager.listAllTargets("DEFAULT", -1).size()); + Assert.assertThrows(IllegalArgumentException.class, () -> { + authManager.deleteTarget("DEFAULT", id); + }); + Assert.assertNotNull(authManager.getTarget(id)); + } + + private static final class LegacyTargetSchema extends SchemaDefine { + + private LegacyTargetSchema(HugeGraphParams graph) { + super(graph, HugeTarget.P.TARGET); + } + + @Override + public void initSchemaIfNeeded() { + List properties = new ArrayList<>(); + properties.add(createPropertyKey(HugeTarget.P.NAME)); + properties.add(createPropertyKey(HugeTarget.P.GRAPH)); + properties.add(createPropertyKey(HugeTarget.P.URL)); + properties.add(createPropertyKey(HugeTarget.P.RESS)); + String[] all = super.initProperties(properties); + + VertexLabel label = this.schema() + .vertexLabel(HugeTarget.P.TARGET) + .properties(all) + .usePrimaryKeyId() + .primaryKeys(HugeTarget.P.NAME) + .nullableKeys(HugeTarget.P.RESS) + .enableLabelIndex(true) + .build(); + this.graph.schemaTransaction().addVertexLabel(label); + } + } + @Test public void testCreateTargetWithRess() { HugeGraph graph = graph(); From 988d016b40649b3d3f8458851a800d3cde3905ca Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 15 Jul 2026 09:25:55 +0800 Subject: [PATCH 10/15] fix(auth): reject foreign scoped access targets --- .../apache/hugegraph/api/auth/AccessAPI.java | 24 +++++++++++++++--- .../api/auth/GraphSpaceAuthPayloadTest.java | 25 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java index f585d15747..a9f812d95e 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java @@ -22,8 +22,10 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; +import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeAccess; import org.apache.hugegraph.auth.HugePermission; +import org.apache.hugegraph.auth.HugeTarget; import org.apache.hugegraph.backend.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; @@ -73,12 +75,28 @@ public String create(@Context GraphManager manager, checkCreatingBody(jsonAccess); HugeAccess access = jsonAccess.build(graphSpace); - GraphSpaceGroupAPI.checkScopedGroupReference( - manager.authManager(), graphSpace, access.source()); - access.id(manager.authManager().createAccess(graphSpace, access)); + access.id(createScopedAccess(manager.authManager(), graphSpace, + access)); return manager.serializer().writeAuthElement(access); } + static Id createScopedAccess(AuthManager authManager, String graphSpace, + HugeAccess access) { + GraphSpaceGroupAPI.checkScopedGroupReference( + authManager, graphSpace, access.source()); + HugeTarget target; + try { + target = authManager.getTarget(graphSpace, access.target()); + } catch (NotFoundException e) { + throw new IllegalArgumentException( + "Invalid target id: " + access.target()); + } + E.checkArgument(target != null, "Invalid target id: %s", + access.target()); + TargetAPI.checkGraphSpace(graphSpace, target); + return authManager.createAccess(graphSpace, access); + } + @PUT @Timed @Path("{id}") diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java index 06c89f9187..3e7b0f72a8 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java @@ -19,6 +19,7 @@ import java.util.Map; +import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeAccess; import org.apache.hugegraph.auth.HugeBelong; import org.apache.hugegraph.auth.HugePermission; @@ -26,6 +27,7 @@ import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.testutil.Assert; import org.junit.Test; +import org.mockito.Mockito; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.ws.rs.ForbiddenException; @@ -96,6 +98,29 @@ public void testAccessRejectsForeignGraphSpace() { AccessAPI.checkGraphSpace("SPACE_B", access)); } + @Test + public void testCreateAccessRejectsForeignTargetWithoutSideEffects() + throws Exception { + AuthManager auth = Mockito.mock(AuthManager.class); + AccessAPI.JsonAccess jsonAccess = new ObjectMapper().readValue( + "{\"group\":\"group\",\"target\":\"target\"," + + "\"access_permission\":\"READ\"}", + AccessAPI.JsonAccess.class); + HugeAccess access = jsonAccess.build("SPACE_A"); + HugeTarget target = new HugeTarget("target", "hugegraph", ""); + target.graphSpace("SPACE_B"); + Mockito.when(auth.getTarget("SPACE_A", access.target())) + .thenReturn(target); + + Assert.assertThrows(ForbiddenException.class, () -> + AccessAPI.createScopedAccess(auth, "SPACE_A", access)); + + Mockito.verify(auth).getTarget("SPACE_A", access.target()); + Mockito.verify(auth, Mockito.never()) + .createAccess(Mockito.anyString(), + Mockito.any(HugeAccess.class)); + } + @Test public void testBelongRejectsForeignGraphSpace() { HugeBelong belong = new HugeBelong("SPACE_A", From 674771ba7998bd68e530421398f9e68cdece1006 Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 15 Jul 2026 09:35:10 +0800 Subject: [PATCH 11/15] test(auth): isolate target schema upgrade fixture --- .../org/apache/hugegraph/auth/HugeTarget.java | 8 +++++-- .../org/apache/hugegraph/core/AuthTest.java | 21 ++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java index 446bfd2445..34ff5aa87d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java @@ -294,10 +294,14 @@ public static String unhide(String key) { } } - public static final class Schema extends SchemaDefine { + public static class Schema extends SchemaDefine { public Schema(HugeGraphParams graph) { - super(graph, P.TARGET); + this(graph, P.TARGET); + } + + protected Schema(HugeGraphParams graph, String label) { + super(graph, label); } @Override diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java index e4fc90455b..aa4ac8cce7 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java @@ -40,7 +40,6 @@ import org.apache.hugegraph.auth.HugeTarget; import org.apache.hugegraph.auth.HugeUser; import org.apache.hugegraph.auth.RolePermission; -import org.apache.hugegraph.auth.SchemaDefine; import org.apache.hugegraph.auth.StandardAuthManager; import org.apache.hugegraph.auth.UserWithRole; import org.apache.hugegraph.backend.cache.Cache; @@ -579,17 +578,17 @@ public void testTargetScopedRoundTripAfterAuthManagerReopen() { @Test public void testTargetSchemaUpgradeAddsNullableScopedProperties() { LegacyTargetSchema schema = new LegacyTargetSchema(params()); - schema.initSchemaIfNeeded(); - VertexLabel legacy = graph().vertexLabel(HugeTarget.P.TARGET); + schema.initLegacySchema(); + VertexLabel legacy = graph().vertexLabel(LegacyTargetSchema.LABEL); List legacyProperties = graph().mapPkId2Name( legacy.properties()); Assert.assertFalse(legacyProperties.contains(HugeTarget.P.GRAPHSPACE)); Assert.assertFalse(legacyProperties.contains( HugeTarget.P.DESCRIPTION)); - HugeTarget.schema(params()).initSchemaIfNeeded(); + schema.initSchemaIfNeeded(); - VertexLabel upgraded = graph().vertexLabel(HugeTarget.P.TARGET); + VertexLabel upgraded = graph().vertexLabel(LegacyTargetSchema.LABEL); Id graphSpace = graph().propertyKey(HugeTarget.P.GRAPHSPACE).id(); Id description = graph().propertyKey(HugeTarget.P.DESCRIPTION).id(); Assert.assertTrue(upgraded.properties().contains(graphSpace)); @@ -722,14 +721,16 @@ public void testStandaloneScopedReadsHidePreexistingForeignTarget() { Assert.assertNotNull(authManager.getTarget(id)); } - private static final class LegacyTargetSchema extends SchemaDefine { + private static final class LegacyTargetSchema extends HugeTarget.Schema { + + private static final String LABEL = HugeTarget.P.TARGET + + "_upgrade_test"; private LegacyTargetSchema(HugeGraphParams graph) { - super(graph, HugeTarget.P.TARGET); + super(graph, LABEL); } - @Override - public void initSchemaIfNeeded() { + private void initLegacySchema() { List properties = new ArrayList<>(); properties.add(createPropertyKey(HugeTarget.P.NAME)); properties.add(createPropertyKey(HugeTarget.P.GRAPH)); @@ -738,7 +739,7 @@ public void initSchemaIfNeeded() { String[] all = super.initProperties(properties); VertexLabel label = this.schema() - .vertexLabel(HugeTarget.P.TARGET) + .vertexLabel(LABEL) .properties(all) .usePrimaryKeyId() .primaryKeys(HugeTarget.P.NAME) From 6ebd15c40571172eecf5f1e8404e26c6ee7ef890 Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 15 Jul 2026 22:02:24 +0800 Subject: [PATCH 12/15] fix(auth): enforce graphspace isolation - normalize legacy scoped target metadata - cascade scoped group relationship cleanup - apply scoped filtering before limits - add auth isolation regression coverage --- .../apache/hugegraph/api/auth/AccessAPI.java | 35 +++-- .../apache/hugegraph/api/auth/BelongAPI.java | 25 ++-- .../api/auth/GraphSpaceGroupAPI.java | 45 +++--- .../apache/hugegraph/api/auth/TargetAPI.java | 18 ++- .../apache/hugegraph/auth/AuthManager.java | 5 + .../hugegraph/auth/StandardAuthManager.java | 6 + .../hugegraph/auth/StandardAuthManagerV2.java | 88 +++++++++++- .../meta/managers/AuthMetaManager.java | 34 +++-- .../api/auth/GraphSpaceAuthPayloadTest.java | 68 +++++++++ .../api/auth/GraphSpaceGroupAPITest.java | 10 ++ .../auth/StandardAuthManagerV2Test.java | 133 ++++++++++++++++++ .../meta/managers/AuthMetaManagerTest.java | 79 +++++++++++ .../apache/hugegraph/unit/UnitTestSuite.java | 12 +- 13 files changed, 494 insertions(+), 64 deletions(-) create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManagerTest.java diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java index a9f812d95e..0568b2e6a9 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java @@ -145,23 +145,31 @@ public String list(@Context GraphManager manager, E.checkArgument(group == null || target == null, "Can't pass both group and target at the same time"); - List belongs; + Id groupId = group == null ? null : UserAPI.parseId(group); + Id targetId = target == null ? null : UserAPI.parseId(target); + List accesses = listScopedAccesses(manager.authManager(), + graphSpace, groupId, + targetId, limit); + return manager.serializer().writeAuthElements("accesses", accesses); + } + + static List listScopedAccesses(AuthManager authManager, + String graphSpace, Id group, + Id target, long limit) { + List accesses; if (group != null) { - Id id = UserAPI.parseId(group); - belongs = manager.authManager().listAccessByGroup(graphSpace, id, - limit); + accesses = authManager.listAccessByGroup(graphSpace, group, -1L); } else if (target != null) { - Id id = UserAPI.parseId(target); - belongs = manager.authManager().listAccessByTarget(graphSpace, id, - limit); + accesses = authManager.listAccessByTarget(graphSpace, target, + -1L); } else { - belongs = manager.authManager().listAllAccess(graphSpace, limit); + accesses = authManager.listAllAccess(graphSpace, -1L); } - belongs = belongs.stream() - .filter(access -> graphSpace.equals( - access.graphSpace())) - .collect(Collectors.toList()); - return manager.serializer().writeAuthElements("accesses", belongs); + accesses = accesses.stream() + .filter(access -> graphSpace.equals( + access.graphSpace())) + .collect(Collectors.toList()); + return GraphSpaceGroupAPI.applyLimit(accesses, limit); } @GET @@ -206,6 +214,7 @@ public void delete(@Context GraphManager manager, } static void checkGraphSpace(String graphSpace, HugeAccess access) { + E.checkArgumentNotNull(access, "The access can't be null"); if (!graphSpace.equals(access.graphSpace())) { throw new jakarta.ws.rs.ForbiddenException( "Permission denied: access belongs to another graphspace"); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java index d092fd6efa..d5e3f105bb 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java @@ -22,6 +22,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; +import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeBelong; import org.apache.hugegraph.backend.id.Id; import org.apache.hugegraph.core.GraphManager; @@ -126,23 +127,30 @@ public String list(@Context GraphManager manager, E.checkArgument(user == null || group == null, "Can't pass both user and group at the same time"); + Id userId = user == null ? null : UserAPI.parseId(user); + Id groupId = group == null ? null : UserAPI.parseId(group); + List belongs = listScopedBelongs(manager.authManager(), + graphSpace, userId, + groupId, limit); + return manager.serializer().writeAuthElements("belongs", belongs); + } + + static List listScopedBelongs(AuthManager authManager, + String graphSpace, Id user, + Id group, long limit) { List belongs; if (user != null) { - Id id = UserAPI.parseId(user); - belongs = manager.authManager().listBelongByUser(graphSpace, id, - limit); + belongs = authManager.listBelongByUser(graphSpace, user, -1L); } else if (group != null) { - Id id = UserAPI.parseId(group); - belongs = manager.authManager().listBelongByGroup(graphSpace, id, - limit); + belongs = authManager.listBelongByGroup(graphSpace, group, -1L); } else { - belongs = manager.authManager().listAllBelong(graphSpace, limit); + belongs = authManager.listAllBelong(graphSpace, -1L); } belongs = belongs.stream() .filter(belong -> graphSpace.equals( belong.graphSpace())) .collect(Collectors.toList()); - return manager.serializer().writeAuthElements("belongs", belongs); + return GraphSpaceGroupAPI.applyLimit(belongs, limit); } @GET @@ -187,6 +195,7 @@ public void delete(@Context GraphManager manager, } static void checkGraphSpace(String graphSpace, HugeBelong belong) { + E.checkArgumentNotNull(belong, "The belong can't be null"); if (!graphSpace.equals(belong.graphSpace())) { throw new jakarta.ws.rs.ForbiddenException( "Permission denied: belong belongs to another graphspace"); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java index b9e54218e7..15d0ea653e 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java @@ -17,9 +17,7 @@ package org.apache.hugegraph.api.auth; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.UUID; @@ -66,7 +64,6 @@ public class GraphSpaceGroupAPI extends API { private static final Logger LOG = Log.logger(GraphSpaceGroupAPI.class); - private static final String SCOPED_PREFIX = "~hubble_role:v1:"; @POST @Timed @@ -97,7 +94,7 @@ public String update(@Context GraphManager manager, LOG.debug("GraphSpace [{}] update scoped group", graphSpace); ensureManager(manager, graphSpace); checkUpdatingBody(jsonGroup); - HugeGroup group = getGroup(manager, id); + HugeGroup group = getGroup(manager.authManager(), id); checkScopedGroup(graphSpace, group); group = jsonGroup.build(group); manager.authManager().updateGroup(group); @@ -119,14 +116,18 @@ public String list(@Context GraphManager manager, static List listScopedGroups(AuthManager authManager, String graphSpace, long limit) { - E.checkArgument(limit >= -1L, - "The limit must be -1 or a non-negative number"); List groups = authManager.listAllGroups(-1); groups = filterScopedGroups(graphSpace, groups); - if (limit >= 0L && groups.size() > limit) { - return new ArrayList<>(groups.subList(0, (int) limit)); + return applyLimit(groups, limit); + } + + static List applyLimit(List values, long limit) { + E.checkArgument(limit >= -1L, + "The limit must be -1 or a non-negative number"); + if (limit >= 0L && values.size() > limit) { + return new ArrayList<>(values.subList(0, (int) limit)); } - return groups; + return values; } @GET @@ -139,7 +140,7 @@ public String get(@Context GraphManager manager, @PathParam("id") String id) { LOG.debug("GraphSpace [{}] get scoped group", graphSpace); ensureManager(manager, graphSpace); - HugeGroup group = getGroup(manager, id); + HugeGroup group = getGroup(manager.authManager(), id); checkScopedGroup(graphSpace, group); return manager.serializer().writeAuthElement(group); } @@ -154,9 +155,9 @@ public void delete(@Context GraphManager manager, @PathParam("id") String id) { LOG.debug("GraphSpace [{}] delete scoped group", graphSpace); ensureManager(manager, graphSpace); - HugeGroup group = getGroup(manager, id); + HugeGroup group = getGroup(manager.authManager(), id); checkScopedGroup(graphSpace, group); - manager.authManager().deleteGroup(group.id()); + manager.authManager().deleteGroup(graphSpace, group.id()); } static void checkManagerPermission(AuthManager authManager, @@ -185,10 +186,7 @@ static void checkScopedGroup(String graphSpace, HugeGroup group) { } static String scopedPrefix(String graphSpace) { - String encoded = Base64.getUrlEncoder().withoutPadding() - .encodeToString(graphSpace.getBytes( - StandardCharsets.UTF_8)); - return SCOPED_PREFIX + encoded + ":"; + return StandardAuthManagerV2.scopedGroupPrefix(graphSpace); } static void ensureManager(GraphManager manager, String graphSpace) { @@ -241,23 +239,20 @@ static void checkScopedGroupReference(AuthManager authManager, } } - private static HugeGroup getGroup(GraphManager manager, String id) { + static HugeGroup getGroup(AuthManager authManager, String id) { + HugeGroup group; try { - return manager.authManager().getGroup(UserAPI.parseId(id)); + group = authManager.getGroup(UserAPI.parseId(id)); } catch (NotFoundException e) { throw new IllegalArgumentException("Invalid group id: " + id); } + E.checkArgument(group != null, "Invalid group id: %s", id); + return group; } private static boolean isScopedGroup(String graphSpace, HugeGroup group) { - if (group == null || group.name() == null) { - return false; - } - String prefix = scopedPrefix(graphSpace); - String name = group.name(); - return name.startsWith(prefix) && - name.substring(prefix.length()).matches("[0-9a-f]{32}"); + return StandardAuthManagerV2.isScopedGroup(graphSpace, group); } @JsonIgnoreProperties(value = {"id", "group_creator", diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/TargetAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/TargetAPI.java index ed379d2444..a8752644f7 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/TargetAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/TargetAPI.java @@ -23,6 +23,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; +import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeTarget; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; @@ -116,14 +117,20 @@ public String list(@Context GraphManager manager, LOG.debug("GraphSpace [{}] list targets", graphSpace); GraphSpaceGroupAPI.ensureAuthManager(manager, graphSpace); - List targets = manager.authManager() - .listAllTargets(graphSpace, - limit); + List targets = listScopedTargets(manager.authManager(), + graphSpace, limit); + return manager.serializer().writeAuthElements("targets", targets); + } + + static List listScopedTargets(AuthManager authManager, + String graphSpace, + long limit) { + List targets = authManager.listAllTargets(graphSpace, -1L); targets = targets.stream() .filter(target -> graphSpace.equals( target.graphSpace())) .collect(Collectors.toList()); - return manager.serializer().writeAuthElements("targets", targets); + return GraphSpaceGroupAPI.applyLimit(targets, limit); } @GET @@ -168,6 +175,7 @@ public void delete(@Context GraphManager manager, } static void checkGraphSpace(String graphSpace, HugeTarget target) { + E.checkArgumentNotNull(target, "The target can't be null"); if (!graphSpace.equals(target.graphSpace())) { throw new jakarta.ws.rs.ForbiddenException( "Permission denied: target belongs to another graphspace"); @@ -185,7 +193,7 @@ static class JsonTarget implements Checkable { @Schema(description = "The target graph name", required = true) private String graph; @JsonProperty("target_url") - @Schema(description = "The target URL", required = true) + @Schema(description = "The target URL") private String url; @JsonProperty("target_description") @Schema(description = "The target description") diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java index 18a2e3e71a..e9ffabe7eb 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java @@ -53,6 +53,11 @@ public interface AuthManager { HugeGroup deleteGroup(Id id); + default HugeGroup deleteGroup(String graphSpace, Id id) { + throw new UnsupportedOperationException( + "Scoped group deletion is not supported"); + } + HugeGroup getGroup(Id id); List listGroups(List ids); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java index 99e00dd0ac..5e76ed5c69 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java @@ -241,6 +241,12 @@ public HugeGroup deleteGroup(Id id) { return this.groups.delete(id); } + @Override + public HugeGroup deleteGroup(String graphSpace, Id id) { + this.checkGraphSpace(graphSpace); + return this.deleteGroup(id); + } + @Override public HugeGroup getGroup(Id id) { return this.groups.get(id); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java index 626d9359b1..57f8dee0dd 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java @@ -18,9 +18,11 @@ package org.apache.hugegraph.auth; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -66,6 +68,7 @@ public class StandardAuthManagerV2 implements AuthManager { private static final long AUTH_TOKEN_EXPIRE = 3600 * 24L; private static final String DEFAULT_ADMIN_ROLE_KEY = "DEFAULT_ADMIN_ROLE"; private static final String DEFAULT_ADMIN_TARGET_KEY = "DEFAULT_ADMIN_TARGET"; + private static final String SCOPED_GROUP_PREFIX = "~hubble_role:v1:"; // Cache private final Cache usersCache; // Cache @@ -79,6 +82,64 @@ public class StandardAuthManagerV2 implements AuthManager { private final MetaManager metaManager = MetaManager.instance(); private final String graphSpace; + static void checkGraphSpace(String graphSpace, String entityGraphSpace) { + E.checkArgument(graphSpace != null && + graphSpace.equals(entityGraphSpace), + "The entity graphspace '%s' does not match '%s'", + entityGraphSpace, graphSpace); + } + + public static String scopedGroupPrefix(String graphSpace) { + String encoded = Base64.getUrlEncoder().withoutPadding() + .encodeToString(graphSpace.getBytes( + StandardCharsets.UTF_8)); + return SCOPED_GROUP_PREFIX + encoded + ":"; + } + + public static boolean isScopedGroup(String graphSpace, HugeGroup group) { + if (group == null || group.name() == null) { + return false; + } + String prefix = scopedGroupPrefix(graphSpace); + String name = group.name(); + return name.startsWith(prefix) && + name.substring(prefix.length()).matches("[0-9a-f]{32}"); + } + + static void checkScopedGroup(String graphSpace, HugeGroup group) { + E.checkArgument(isScopedGroup(graphSpace, group), + "The group does not belong to graphspace '%s'", + graphSpace); + } + + static void deleteGroupRelations(AuthManager authManager, + String graphSpace, Id group) { + Set belongs = new HashSet<>(); + for (HugeBelong belong : authManager.listBelongByUser( + graphSpace, group, -1L)) { + if (HugeBelong.GR.equals(belong.link()) && + group.equals(belong.source())) { + belongs.add(belong.id()); + } + } + for (HugeBelong belong : authManager.listBelongByGroup( + graphSpace, group, -1L)) { + if (HugeBelong.UG.equals(belong.link()) && + group.equals(belong.target())) { + belongs.add(belong.id()); + } + } + for (Id belong : belongs) { + authManager.deleteBelong(graphSpace, belong); + } + for (HugeAccess access : authManager.listAccessByGroup( + graphSpace, group, -1L)) { + if (group.equals(access.source())) { + authManager.deleteAccess(graphSpace, access.id()); + } + } + } + public StandardAuthManagerV2(HugeGraphParams graph) { E.checkNotNull(graph, "graph"); HugeConfig config = graph.configuration(); @@ -254,7 +315,7 @@ protected void deleteBelongsByUserOrGroup(Id id) { HugeBelong.ALL, -1); for (HugeBelong belong : belongs) { - this.deleteBelong(belong.id()); + this.deleteBelong(space, belong.id()); } } @@ -417,6 +478,24 @@ public HugeGroup deleteGroup(Id id) { } } + @Override + public HugeGroup deleteGroup(String graphSpace, Id id) { + HugeGroup group = this.getGroup(id); + checkScopedGroup(graphSpace, group); + deleteGroupRelations(this, graphSpace, id); + try { + HugeGroup result = this.metaManager.deleteGroup(id); + this.invalidateUserCache(); + return result; + } catch (IOException e) { + throw new HugeException("IOException occurs when " + + "deserialize group", e); + } catch (ClassNotFoundException e) { + throw new HugeException("ClassNotFoundException occurs when " + + "deserialize group", e); + } + } + @Override public HugeGroup getGroup(Id id) { try { @@ -467,6 +546,7 @@ public Id createTarget(HugeTarget target) { @Override public Id createTarget(String graphSpace, HugeTarget target) { + checkGraphSpace(graphSpace, target.graphSpace()); try { target.create(target.update()); updateCreator(target); @@ -486,6 +566,7 @@ public Id updateTarget(HugeTarget target) { @Override public Id updateTarget(String graphSpace, HugeTarget target) { + checkGraphSpace(graphSpace, target.graphSpace()); try { HugeTarget result = this.metaManager.updateTarget(graphSpace, target); this.invalidateUserCache(); @@ -577,6 +658,7 @@ public Id createBelong(HugeBelong belong) { @Override public Id createBelong(String graphSpace, HugeBelong belong) { + checkGraphSpace(graphSpace, belong.graphSpace()); try { belong.create(belong.update()); updateCreator(belong); @@ -598,6 +680,7 @@ public Id updateBelong(HugeBelong belong) { @Override public Id updateBelong(String graphSpace, HugeBelong belong) { + checkGraphSpace(graphSpace, belong.graphSpace()); try { HugeBelong result = this.metaManager.updateBelong(graphSpace, belong); this.invalidateUserCache(); @@ -727,6 +810,7 @@ public Id createAccess(HugeAccess access) { @Override public Id createAccess(String graphSpace, HugeAccess access) { + checkGraphSpace(graphSpace, access.graphSpace()); try { access.create(access.update()); updateCreator(access); @@ -749,6 +833,7 @@ public Id updateAccess(HugeAccess access) { @Override public Id updateAccess(String graphSpace, HugeAccess access) { + checkGraphSpace(graphSpace, access.graphSpace()); HugeAccess result = null; try { result = this.metaManager.updateAccess(graphSpace, access); @@ -1569,6 +1654,7 @@ private void tryInitAdminRole() { if (target == null) { target = new HugeTarget(DEFAULT_ADMIN_TARGET_KEY, ALL_GRAPH_SPACES, ALL_GRAPHS); + target.graphSpace(ALL_GRAPH_SPACES); this.updateCreator(target); target.create(target.update()); this.metaManager.createTarget(ALL_GRAPH_SPACES, target); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManager.java index 7b167dea9d..bf801c0849 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManager.java @@ -33,6 +33,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -52,6 +53,7 @@ import org.apache.hugegraph.meta.MetaManager; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.JsonUtil; +import org.apache.tinkerpop.gremlin.structure.Graph.Hidden; public class AuthMetaManager extends AbstractMetaManager { @@ -372,7 +374,7 @@ public HugeTarget updateTarget(String graphSpace, HugeTarget target) // only url, graph, description, resources and update-time could be updated Map map = JsonUtil.fromJson(result, Map.class); - HugeTarget ori = HugeTarget.fromMap(map); + HugeTarget ori = deserializeTarget(graphSpace, map); ori.update(new Date()); ori.url(target.url()); ori.graph(target.graph()); @@ -393,10 +395,12 @@ public HugeTarget deleteTarget(String graphSpace, Id id) id.asString())); E.checkArgument(StringUtils.isNotEmpty(result), "The target name '%s' is not existed", id.asString()); - this.metaDriver.delete(targetKey(graphSpace, id.asString())); - this.putAuthEvent(new MetaManager.AuthEvent("DELETE", "TARGET", id.asString())); Map map = JsonUtil.fromJson(result, Map.class); - return HugeTarget.fromMap(map); + HugeTarget target = deserializeTarget(graphSpace, map); + this.metaDriver.delete(targetKey(graphSpace, id.asString())); + this.putAuthEvent(new MetaManager.AuthEvent("DELETE", "TARGET", + id.asString())); + return target; } @SuppressWarnings("unchecked") @@ -407,7 +411,7 @@ public HugeTarget findTarget(String graphSpace, Id id) { return null; } Map map = JsonUtil.fromJson(result, Map.class); - return HugeTarget.fromMap(map); + return deserializeTarget(graphSpace, map); } @SuppressWarnings("unchecked") @@ -419,7 +423,7 @@ public HugeTarget getTarget(String graphSpace, Id id) E.checkArgument(StringUtils.isNotEmpty(result), "The target name '%s' is not existed", id.asString()); Map map = JsonUtil.fromJson(result, Map.class); - return HugeTarget.fromMap(map); + return deserializeTarget(graphSpace, map); } @SuppressWarnings("unchecked") @@ -435,7 +439,7 @@ public List listTargets(String graphSpace, List ids) id.asString())); Map map = JsonUtil.fromJson(targetString, Map.class); - HugeTarget target = HugeTarget.fromMap(map); + HugeTarget target = deserializeTarget(graphSpace, map); result.add(target); } } @@ -456,13 +460,27 @@ public List listAllTargets(String graphSpace, long limit) } Map map = JsonUtil.fromJson(item.getValue(), Map.class); - HugeTarget target = HugeTarget.fromMap(map); + HugeTarget target = deserializeTarget(graphSpace, map); result.add(target); } return result; } + private static HugeTarget deserializeTarget(String graphSpace, + Map map) { + String key = Hidden.unHide(HugeTarget.P.GRAPHSPACE); + Map scoped = new HashMap<>(map); + if (!scoped.containsKey(key)) { + scoped.put(key, graphSpace); + } else { + E.checkArgument(graphSpace.equals(scoped.get(key)), + "The target graphspace '%s' does not match '%s'", + scoped.get(key), graphSpace); + } + return HugeTarget.fromMap(scoped); + } + public Id createBelong(String graphSpace, HugeBelong belong) throws IOException, ClassNotFoundException { String belongId = this.checkBelong(graphSpace, belong); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java index 3e7b0f72a8..8fd11f37b5 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.auth; +import java.util.Arrays; +import java.util.List; import java.util.Map; import org.apache.hugegraph.auth.AuthManager; @@ -132,4 +134,70 @@ public void testBelongRejectsForeignGraphSpace() { Assert.assertThrows(ForbiddenException.class, () -> BelongAPI.checkGraphSpace("SPACE_B", belong)); } + + @Test + public void testScopedChecksRejectMissingEntities() { + Assert.assertThrows(IllegalArgumentException.class, () -> + TargetAPI.checkGraphSpace("SPACE_A", null)); + Assert.assertThrows(IllegalArgumentException.class, () -> + BelongAPI.checkGraphSpace("SPACE_A", null)); + Assert.assertThrows(IllegalArgumentException.class, () -> + AccessAPI.checkGraphSpace("SPACE_A", null)); + } + + @Test + public void testScopedListsFilterBeforeApplyingLimit() { + AuthManager auth = Mockito.mock(AuthManager.class); + HugeTarget foreignTarget = target("SPACE_B", "foreign"); + HugeTarget ownTarget1 = target("SPACE_A", "own-1"); + HugeTarget ownTarget2 = target("SPACE_A", "own-2"); + Mockito.when(auth.listAllTargets("SPACE_A", -1L)).thenReturn( + Arrays.asList(foreignTarget, ownTarget1, ownTarget2)); + + HugeBelong foreignBelong = belong("SPACE_B", "foreign"); + HugeBelong ownBelong1 = belong("SPACE_A", "own-1"); + HugeBelong ownBelong2 = belong("SPACE_A", "own-2"); + Mockito.when(auth.listAllBelong("SPACE_A", -1L)).thenReturn( + Arrays.asList(foreignBelong, ownBelong1, ownBelong2)); + + HugeAccess foreignAccess = access("SPACE_B", "foreign"); + HugeAccess ownAccess1 = access("SPACE_A", "own-1"); + HugeAccess ownAccess2 = access("SPACE_A", "own-2"); + Mockito.when(auth.listAllAccess("SPACE_A", -1L)).thenReturn( + Arrays.asList(foreignAccess, ownAccess1, ownAccess2)); + + List targets = TargetAPI.listScopedTargets( + auth, "SPACE_A", 1L); + List belongs = BelongAPI.listScopedBelongs( + auth, "SPACE_A", null, null, 1L); + List accesses = AccessAPI.listScopedAccesses( + auth, "SPACE_A", null, null, 1L); + + Assert.assertEquals(Arrays.asList(ownTarget1), targets); + Assert.assertEquals(Arrays.asList(ownBelong1), belongs); + Assert.assertEquals(Arrays.asList(ownAccess1), accesses); + } + + private static HugeTarget target(String graphSpace, String name) { + HugeTarget target = new HugeTarget(name, "hugegraph", ""); + target.graphSpace(graphSpace); + target.creator("admin"); + return target; + } + + private static HugeBelong belong(String graphSpace, String suffix) { + HugeBelong belong = new HugeBelong( + graphSpace, IdGenerator.of("user-" + suffix), + IdGenerator.of("group-" + suffix), null, HugeBelong.UG); + belong.creator("admin"); + return belong; + } + + private static HugeAccess access(String graphSpace, String suffix) { + HugeAccess access = new HugeAccess( + graphSpace, IdGenerator.of("group-" + suffix), + IdGenerator.of("target-" + suffix)); + access.creator("admin"); + return access; + } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java index a3b2475638..f2b261159d 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPITest.java @@ -125,6 +125,16 @@ public void testScopedGroupValidationRejectsForeignAndLegacyGroup() { }); } + @Test + public void testGetGroupRejectsMissingV2Result() { + AuthManager auth = Mockito.mock(AuthManager.class); + Mockito.when(auth.getGroup(Mockito.any())).thenReturn(null); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + GraphSpaceGroupAPI.getGroup(auth, "missing"); + }); + } + private static HugeGroup group(String graphSpace, char suffix) { return new HugeGroup(GraphSpaceGroupAPI.scopedPrefix(graphSpace) + repeat(suffix, 32)); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java new file mode 100644 index 0000000000..e493ecbec3 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.auth; + +import java.util.Arrays; +import java.util.Collections; + +import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class StandardAuthManagerV2Test { + + @Test + public void testScopedWriteRejectsMismatchedEntityGraphSpace() { + StandardAuthManagerV2.checkGraphSpace("SPACE_A", "SPACE_A"); + Assert.assertThrows(IllegalArgumentException.class, () -> { + StandardAuthManagerV2.checkGraphSpace("SPACE_A", "SPACE_B"); + }); + } + + @Test + public void testScopedGroupValidationRejectsForeignOrMissingGroup() { + HugeGroup own = group("SPACE_A", 'a'); + HugeGroup foreign = group("SPACE_B", 'b'); + + StandardAuthManagerV2.checkScopedGroup("SPACE_A", own); + Assert.assertThrows(IllegalArgumentException.class, () -> { + StandardAuthManagerV2.checkScopedGroup("SPACE_A", foreign); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + StandardAuthManagerV2.checkScopedGroup("SPACE_A", null); + }); + } + + @Test + public void testDeleteGroupRelationsCascadesWithinGraphSpace() { + AuthManager auth = Mockito.mock(AuthManager.class); + Id group = IdGenerator.of("group"); + HugeBelong groupRole = groupRole("group-role", group); + HugeBelong userGroup = userGroup("user-group", group); + HugeBelong sourceCollision = userGroup( + "source-collision", group, IdGenerator.of("other-group")); + HugeBelong targetCollision = userRole("target-collision", group); + HugeAccess access = access("access", group); + + Mockito.when(auth.listBelongByUser("SPACE_A", group, -1L)) + .thenReturn(Arrays.asList(groupRole, sourceCollision)); + Mockito.when(auth.listBelongByGroup("SPACE_A", group, -1L)) + .thenReturn(Arrays.asList(userGroup, targetCollision)); + Mockito.when(auth.listAccessByGroup("SPACE_A", group, -1L)) + .thenReturn(Collections.singletonList(access)); + + StandardAuthManagerV2.deleteGroupRelations(auth, "SPACE_A", group); + + Mockito.verify(auth).listBelongByUser("SPACE_A", group, -1L); + Mockito.verify(auth).listBelongByGroup("SPACE_A", group, -1L); + Mockito.verify(auth).listAccessByGroup("SPACE_A", group, -1L); + Mockito.verify(auth).deleteBelong("SPACE_A", groupRole.id()); + Mockito.verify(auth).deleteBelong("SPACE_A", userGroup.id()); + Mockito.verify(auth, Mockito.never()) + .deleteBelong("SPACE_A", sourceCollision.id()); + Mockito.verify(auth, Mockito.never()) + .deleteBelong("SPACE_A", targetCollision.id()); + Mockito.verify(auth).deleteAccess("SPACE_A", access.id()); + Mockito.verifyNoMoreInteractions(auth); + } + + private static HugeBelong groupRole(String id, Id group) { + HugeBelong belong = new HugeBelong( + "SPACE_A", null, group, IdGenerator.of("role"), + HugeBelong.GR); + belong.id(IdGenerator.of(id)); + return belong; + } + + private static HugeBelong userGroup(String id, Id group) { + return userGroup(id, IdGenerator.of("user"), group); + } + + private static HugeBelong userGroup(String id, Id user, Id group) { + HugeBelong belong = new HugeBelong( + "SPACE_A", user, group, null, HugeBelong.UG); + belong.id(IdGenerator.of(id)); + return belong; + } + + private static HugeBelong userRole(String id, Id role) { + HugeBelong belong = new HugeBelong( + "SPACE_A", IdGenerator.of("user"), null, role, + HugeBelong.UR); + belong.id(IdGenerator.of(id)); + return belong; + } + + private static HugeAccess access(String id, Id group) { + HugeAccess access = new HugeAccess("SPACE_A", group, + IdGenerator.of("target")); + access.id(IdGenerator.of(id)); + return access; + } + + private static HugeGroup group(String graphSpace, char suffix) { + String name = StandardAuthManagerV2.scopedGroupPrefix(graphSpace) + + repeat(suffix, 32); + return new HugeGroup(name); + } + + private static String repeat(char value, int count) { + StringBuilder builder = new StringBuilder(count); + for (int i = 0; i < count; i++) { + builder.append(value); + } + return builder.toString(); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManagerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManagerTest.java new file mode 100644 index 0000000000..074753308e --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManagerTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.meta.managers; + +import java.util.Map; + +import org.apache.hugegraph.auth.HugeTarget; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.meta.MetaDriver; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.util.JsonUtil; +import org.junit.Test; +import org.mockito.Mockito; + +public class AuthMetaManagerTest { + + @Test + public void testGetTargetRestoresLegacyGraphSpaceFromNamespace() + throws Exception { + MetaDriver driver = Mockito.mock(MetaDriver.class); + Map legacy = target("DEFAULT").asMap(); + legacy.remove("graphspace"); + Mockito.when(driver.get(Mockito.anyString())) + .thenReturn(JsonUtil.toJson(legacy)); + AuthMetaManager manager = new AuthMetaManager(driver, "cluster"); + + HugeTarget restored = manager.getTarget( + "SPACE_A", IdGenerator.of("target")); + + Assert.assertEquals("SPACE_A", restored.graphSpace()); + } + + @Test + public void testGetTargetRejectsExplicitGraphSpaceMismatch() { + MetaDriver driver = Mockito.mock(MetaDriver.class); + Mockito.when(driver.get(Mockito.anyString())) + .thenReturn(JsonUtil.toJson(target("SPACE_B").asMap())); + AuthMetaManager manager = new AuthMetaManager(driver, "cluster"); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + manager.getTarget("SPACE_A", IdGenerator.of("target")); + }); + } + + @Test + public void testDeleteTargetRejectsMismatchBeforeMutation() { + MetaDriver driver = Mockito.mock(MetaDriver.class); + Mockito.when(driver.get(Mockito.anyString())) + .thenReturn(JsonUtil.toJson(target("SPACE_B").asMap())); + AuthMetaManager manager = new AuthMetaManager(driver, "cluster"); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + manager.deleteTarget("SPACE_A", IdGenerator.of("target")); + }); + Mockito.verify(driver, Mockito.never()).delete(Mockito.anyString()); + } + + private static HugeTarget target(String graphSpace) { + HugeTarget target = new HugeTarget("target", "hugegraph", "url"); + target.graphSpace(graphSpace); + target.creator("admin"); + return target; + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index 3ce9f8b9f1..16fc74c2d0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -17,12 +17,14 @@ package org.apache.hugegraph.unit; -import org.apache.hugegraph.auth.WsAndHttpBasicAuthHandlerTest; -import org.apache.hugegraph.api.auth.GraphSpaceGroupAPITest; import org.apache.hugegraph.api.auth.GraphSpaceAuthPayloadTest; +import org.apache.hugegraph.api.auth.GraphSpaceGroupAPITest; +import org.apache.hugegraph.auth.StandardAuthManagerV2Test; +import org.apache.hugegraph.auth.WsAndHttpBasicAuthHandlerTest; import org.apache.hugegraph.core.RoleElectionStateMachineTest; import org.apache.hugegraph.meta.EtcdMetaDriverTest; import org.apache.hugegraph.meta.MetaManagerSchemaCacheClearEventTest; +import org.apache.hugegraph.meta.managers.AuthMetaManagerTest; import org.apache.hugegraph.traversal.optimize.TraversalUtilOptimizeTest; import org.apache.hugegraph.unit.api.filter.LoadDetectFilterTest; import org.apache.hugegraph.unit.api.filter.PathFilterTest; @@ -100,8 +102,10 @@ /* api gremlin */ GremlinQueryAPITest.class, WsAndHttpBasicAuthHandlerTest.class, - GraphSpaceGroupAPITest.class, - GraphSpaceAuthPayloadTest.class, + GraphSpaceGroupAPITest.class, + GraphSpaceAuthPayloadTest.class, + StandardAuthManagerV2Test.class, + AuthMetaManagerTest.class, /* api space */ GraphSpaceAPITest.class, From 5b082f8da352a2819dcc15950455a657520cb91f Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 19 Jul 2026 04:17:06 +0800 Subject: [PATCH 13/15] fix(auth): preserve identity and redact bearer tokens --- .../apache/hugegraph/api/auth/LoginAPI.java | 4 +- .../hugegraph/auth/HugeGraphAuthProxy.java | 2 +- .../auth/WsAndHttpBasicAuthHandler.java | 6 +- .../hugegraph/auth/StandardAuthManager.java | 2 +- .../hugegraph/auth/StandardAuthManagerV2.java | 3 +- .../auth/StandardAuthManagerV2Test.java | 7 +- .../auth/WsAndHttpBasicAuthHandlerTest.java | 11 +- .../apache/hugegraph/unit/UnitTestSuite.java | 2 + .../hugegraph/unit/api/auth/LoginAPITest.java | 141 ++++++++++++++++++ .../unit/auth/HugeGraphAuthProxyTest.java | 75 ++++++++++ 10 files changed, 243 insertions(+), 10 deletions(-) create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/auth/LoginAPITest.java diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/LoginAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/LoginAPI.java index faf62c4064..d718f3d2b1 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/LoginAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/LoginAPI.java @@ -87,7 +87,7 @@ public void logout(@Context GraphManager manager, @HeaderParam(HttpHeaders.AUTHORIZATION) String auth) { E.checkArgument(StringUtils.isNotEmpty(auth), "Request header Authorization must not be null"); - LOG.debug("user logout: {}", auth); + LOG.debug("User logout requested"); if (!auth.startsWith(AuthenticationFilter.BEARER_TOKEN_PREFIX)) { throw new BadRequestException("Only HTTP Bearer authentication is supported"); @@ -107,7 +107,7 @@ public String verifyToken(@Context GraphManager manager, @HeaderParam(HttpHeaders.AUTHORIZATION) String token) { E.checkArgument(StringUtils.isNotEmpty(token), "Request header Authorization must not be null"); - LOG.debug("get user: {}", token); + LOG.debug("User token verification requested"); if (!token.startsWith(AuthenticationFilter.BEARER_TOKEN_PREFIX)) { throw new BadRequestException("Only HTTP Bearer authentication is supported"); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java index 8dabfe0613..e0a28d8ccd 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java @@ -1872,7 +1872,7 @@ public UserWithRole validateUser(String token) { return this.authManager.validateUser(token); }); } catch (Exception e) { - LOG.error("Failed to validate token {} with error: ", token, e); + LOG.error("Failed to validate token with error: ", e); throw e; } finally { setContext(context); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java index 821c90de46..388d61dd68 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java @@ -31,9 +31,11 @@ import org.apache.tinkerpop.gremlin.server.Settings; import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException; +import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser; import org.apache.tinkerpop.gremlin.server.auth.Authenticator; import org.apache.tinkerpop.gremlin.server.handler.AbstractAuthenticationHandler; import org.apache.tinkerpop.gremlin.server.handler.SaslAuthenticationHandler; +import org.apache.tinkerpop.gremlin.server.handler.StateKey; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler; @@ -132,7 +134,9 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { credentials.put(HugeAuthenticator.KEY_ADDRESS, address); try { - this.authenticator.authenticate(credentials); + AuthenticatedUser user = + this.authenticator.authenticate(credentials); + ctx.channel().attr(StateKey.AUTHENTICATED_USER).set(user); ctx.fireChannelRead(request); } catch (AuthenticationException ae) { sendError(ctx, msg); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java index 5e76ed5c69..b05969c53f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java @@ -854,7 +854,7 @@ public UserWithRole validateUser(String token) { try { payload = this.tokenGenerator.verify(token); } catch (Throwable t) { - LOG.error(String.format("Failed to verify token:[ %s ], cause:", token), t); + LOG.error("Failed to verify token", t); return new UserWithRole(""); } username = (String) payload.get(AuthConstant.TOKEN_USER_NAME); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java index 57f8dee0dd..0cdc08d8c3 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java @@ -107,6 +107,7 @@ public static boolean isScopedGroup(String graphSpace, HugeGroup group) { } static void checkScopedGroup(String graphSpace, HugeGroup group) { + E.checkArgument(group != null, "The group does not exist"); E.checkArgument(isScopedGroup(graphSpace, group), "The group does not belong to graphspace '%s'", graphSpace); @@ -1339,7 +1340,7 @@ public UserWithRole validateUser(String token) { try { payload = this.tokenGenerator.verify(token); } catch (Throwable t) { - LOG.error(String.format("Failed to verify token:[ %s ], cause:", token), t); + LOG.error("Failed to verify token", t); return new UserWithRole(""); } username = (String) payload.get(AuthConstant.TOKEN_USER_NAME); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java index e493ecbec3..1b05e4ee24 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java @@ -45,9 +45,12 @@ public void testScopedGroupValidationRejectsForeignOrMissingGroup() { Assert.assertThrows(IllegalArgumentException.class, () -> { StandardAuthManagerV2.checkScopedGroup("SPACE_A", foreign); }); - Assert.assertThrows(IllegalArgumentException.class, () -> { + try { StandardAuthManagerV2.checkScopedGroup("SPACE_A", null); - }); + Assert.fail("Expected a missing-group error"); + } catch (IllegalArgumentException e) { + Assert.assertContains("does not exist", e.getMessage()); + } } @Test diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java index 0bd8c20e07..9aa0859fc0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java @@ -29,6 +29,7 @@ import org.apache.tinkerpop.gremlin.server.Settings; import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser; import org.apache.tinkerpop.gremlin.server.auth.Authenticator; +import org.apache.tinkerpop.gremlin.server.handler.StateKey; import org.apache.hugegraph.testutil.Assert; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -45,8 +46,9 @@ public class WsAndHttpBasicAuthHandlerTest { public void testBearerTokenAuthenticatesHttpGremlinRequest() throws Exception { Authenticator authenticator = Mockito.mock(Authenticator.class); + AuthenticatedUser user = new AuthenticatedUser("admin"); Mockito.when(authenticator.authenticate(Mockito.anyMap())) - .thenReturn(new AuthenticatedUser("admin")); + .thenReturn(user); WsAndHttpBasicAuthHandler handler = new WsAndHttpBasicAuthHandler(authenticator, new Settings()); EmbeddedChannel channel = new EmbeddedChannel(); @@ -68,6 +70,8 @@ public void testBearerTokenAuthenticatesHttpGremlinRequest() credentials.getValue().containsKey("username")); Assert.assertFalse( credentials.getValue().containsKey("password")); + Assert.assertSame(user, + channel.attr(StateKey.AUTHENTICATED_USER).get()); channel.finishAndReleaseAll(); } @@ -76,8 +80,9 @@ public void testBearerTokenAuthenticatesHttpGremlinRequest() public void testBasicCredentialsStillAuthenticateHttpGremlinRequest() throws Exception { Authenticator authenticator = Mockito.mock(Authenticator.class); + AuthenticatedUser user = new AuthenticatedUser("admin"); Mockito.when(authenticator.authenticate(Mockito.anyMap())) - .thenReturn(new AuthenticatedUser("admin")); + .thenReturn(user); EmbeddedChannel channel = channel(authenticator); String encoded = Base64.getEncoder().encodeToString( "admin:password".getBytes(StandardCharsets.UTF_8)); @@ -91,6 +96,8 @@ public void testBasicCredentialsStillAuthenticateHttpGremlinRequest() Assert.assertEquals("password", credentials.getValue().get("password")); Assert.assertFalse(credentials.getValue().containsKey( HugeAuthenticator.KEY_TOKEN)); + Assert.assertSame(user, + channel.attr(StateKey.AUTHENTICATED_USER).get()); channel.finishAndReleaseAll(); } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index 16fc74c2d0..1a4f0c2c9b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -26,6 +26,7 @@ import org.apache.hugegraph.meta.MetaManagerSchemaCacheClearEventTest; import org.apache.hugegraph.meta.managers.AuthMetaManagerTest; import org.apache.hugegraph.traversal.optimize.TraversalUtilOptimizeTest; +import org.apache.hugegraph.unit.api.auth.LoginAPITest; import org.apache.hugegraph.unit.api.filter.LoadDetectFilterTest; import org.apache.hugegraph.unit.api.filter.PathFilterTest; import org.apache.hugegraph.unit.api.gremlin.GremlinQueryAPITest; @@ -97,6 +98,7 @@ @Suite.SuiteClasses({ /* api filter */ LoadDetectFilterTest.class, + LoginAPITest.class, PathFilterTest.class, /* api gremlin */ diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/auth/LoginAPITest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/auth/LoginAPITest.java new file mode 100644 index 0000000000..bed7ec85ac --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/auth/LoginAPITest.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.unit.api.auth; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import org.apache.hugegraph.api.auth.LoginAPI; +import org.apache.hugegraph.auth.AuthManager; +import org.apache.hugegraph.auth.HugeAuthenticator; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.config.Property; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import sun.misc.Unsafe; + +public class LoginAPITest extends BaseUnitTest { + + private static final String LOGGER_NAME = LoginAPI.class.getName(); + private static final String SECRET = "secret-bearer-token"; + + private LoggerContext loggerContext; + private org.apache.logging.log4j.core.config.Configuration configuration; + private TestAppender appender; + + @Before + public void setup() { + this.appender = new TestAppender(); + this.appender.start(); + this.loggerContext = (LoggerContext) LogManager.getContext(false); + this.configuration = this.loggerContext.getConfiguration(); + LoggerConfig logger = new LoggerConfig(LOGGER_NAME, Level.DEBUG, false); + logger.addAppender(this.appender, Level.DEBUG, null); + this.configuration.addLogger(LOGGER_NAME, logger); + this.loggerContext.updateLoggers(); + } + + @After + public void teardown() { + this.configuration.removeLogger(LOGGER_NAME); + this.loggerContext.updateLoggers(); + this.appender.stop(); + } + + @Test + public void testLogoutDoesNotLogBearerToken() { + AuthManager authManager = Mockito.mock(AuthManager.class); + GraphManager manager = managerWithAuthManager(authManager); + + new LoginAPI().logout(manager, "Bearer " + SECRET); + + assertSecretNotLogged(); + } + + @Test + public void testVerifyDoesNotLogBearerToken() { + AuthManager authManager = Mockito.mock(AuthManager.class); + GraphManager manager = managerWithAuthManager(authManager); + Mockito.when(authManager.validateUser(SECRET)) + .thenThrow(new IllegalArgumentException("invalid token")); + + Assert.assertThrows(IllegalArgumentException.class, + () -> new LoginAPI().verifyToken(manager, + "Bearer " + SECRET)); + + assertSecretNotLogged(); + } + + private void assertSecretNotLogged() { + for (LogEvent event : this.appender.events()) { + Assert.assertFalse(event.getMessage().getFormattedMessage() + .contains(SECRET)); + } + } + + private static GraphManager managerWithAuthManager(AuthManager authManager) { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + Unsafe unsafe = (Unsafe) field.get(null); + GraphManager manager = (GraphManager) unsafe.allocateInstance( + GraphManager.class); + HugeAuthenticator authenticator = Mockito.mock(HugeAuthenticator.class); + Mockito.when(authenticator.authManager()).thenReturn(authManager); + Whitebox.setInternalState(manager, "authenticator", authenticator); + return manager; + } catch (Exception e) { + throw new AssertionError(e); + } + } + + private static class TestAppender extends AbstractAppender { + + private final List events; + + TestAppender() { + super("LoginAPITestAppender", (Filter) null, + (Layout) null, true, Property.EMPTY_ARRAY); + this.events = new ArrayList<>(); + } + + @Override + public void append(LogEvent event) { + this.events.add(event.toImmutable()); + } + + List events() { + return this.events; + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java index e6520b0188..3de789e63b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java @@ -18,6 +18,8 @@ package org.apache.hugegraph.unit.auth; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.auth.AuthManager; @@ -33,6 +35,15 @@ import org.apache.hugegraph.task.TaskScheduler; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.config.Property; import org.junit.After; import org.junit.Test; import org.mockito.Mockito; @@ -244,4 +255,68 @@ public void testDefaultRoleMutationInvalidatesUserRoleCache() Mockito.verify(authManager, Mockito.times(2)) .validateUser("cache_user", "pass"); } + + @Test + public void testValidateUserDoesNotLogBearerToken() { + String token = "secret-proxy-bearer-token"; + HugeGraph graph = Mockito.mock(HugeGraph.class); + HugeConfig config = Mockito.mock(HugeConfig.class); + AuthManager authManager = Mockito.mock(AuthManager.class); + TaskScheduler scheduler = Mockito.mock(TaskScheduler.class); + + Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph"); + Mockito.when(graph.configuration()).thenReturn(config); + Mockito.when(graph.authManager()).thenReturn(authManager); + Mockito.when(graph.taskScheduler()).thenReturn(scheduler); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_EXPIRE)).thenReturn(3600L); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_CAPACITY)).thenReturn(100L); + Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE)).thenReturn(1000D); + Mockito.when(authManager.validateUser(token)) + .thenThrow(new IllegalArgumentException("invalid token")); + + TestAppender appender = new TestAppender(); + appender.start(); + LoggerContext context = (LoggerContext) LogManager.getContext(false); + org.apache.logging.log4j.core.config.Configuration configuration = + context.getConfiguration(); + String loggerName = HugeGraphAuthProxy.class.getName(); + LoggerConfig logger = new LoggerConfig(loggerName, Level.ERROR, false); + logger.addAppender(appender, Level.ERROR, null); + configuration.addLogger(loggerName, logger); + context.updateLoggers(); + try { + AuthManager proxy = new HugeGraphAuthProxy(graph).authManager(); + Assert.assertThrows(IllegalArgumentException.class, + () -> proxy.validateUser(token)); + Assert.assertFalse(appender.events().isEmpty()); + for (LogEvent event : appender.events()) { + Assert.assertFalse(event.getMessage().getFormattedMessage() + .contains(token)); + } + } finally { + configuration.removeLogger(loggerName); + context.updateLoggers(); + appender.stop(); + } + } + + private static class TestAppender extends AbstractAppender { + + private final List events; + + TestAppender() { + super("HugeGraphAuthProxyTestAppender", (Filter) null, + (Layout) null, true, Property.EMPTY_ARRAY); + this.events = new ArrayList<>(); + } + + @Override + public void append(LogEvent event) { + this.events.add(event.toImmutable()); + } + + List events() { + return this.events; + } + } } From f3e0ceff7b8256764d49201bf0020522b2024625 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 19 Jul 2026 16:52:27 +0800 Subject: [PATCH 14/15] fix(auth): tighten scoped access boundaries - delegate graphspace auth operations through the proxy - invalidate cached token roles on logout - hide built-in access metadata from business APIs - require valid Store REST discovery addresses - keep Gremlin HTTP on Basic authentication only --- .../hugegraph/pd/service/SDConfigService.java | 11 +- .../hugegraph/pd/rest/PDRestSuiteTest.java | 2 + .../pd/service/SDConfigServiceTest.java | 48 +++++ .../apache/hugegraph/api/auth/AccessAPI.java | 12 +- .../api/auth/GraphSpaceGroupAPI.java | 45 ++++- .../hugegraph/auth/HugeGraphAuthProxy.java | 164 +++++++++++++++++- .../auth/WsAndHttpBasicAuthHandler.java | 24 ++- .../apache/hugegraph/auth/AuthManager.java | 4 + .../hugegraph/auth/StandardAuthManager.java | 2 + .../hugegraph/auth/StandardAuthManagerV2.java | 5 + .../api/auth/GraphSpaceAuthPayloadTest.java | 39 +++++ .../auth/WsAndHttpBasicAuthHandlerTest.java | 44 ++--- .../unit/auth/HugeGraphAuthProxyTest.java | 66 +++++++ 13 files changed, 405 insertions(+), 61 deletions(-) create mode 100644 hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/SDConfigServiceTest.java diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java index 3fac92efaf..d627d54021 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java @@ -215,15 +215,8 @@ private Set getStoreAddresses() { return res; } - // Keep the legacy gRPC fallback when no valid REST port is registered. - private String getRestAddress(Metapb.Store store) { - String address = store.getAddress(); - if (address == null || address.isEmpty()) { - return null; - } - String restAddress = StoreRestAddressUtil.getRestAddress(store); - return restAddress != null ? restAddress : address; - + static String getRestAddress(Metapb.Store store) { + return StoreRestAddressUtil.getRestAddress(store); } public List getConfigs(String appName, String path) { diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java index 5dba561948..38646b4d91 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.pd.rest; +import org.apache.hugegraph.pd.service.SDConfigServiceTest; import org.apache.hugegraph.pd.util.StoreRestAddressUtilTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -26,6 +27,7 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ RestApiTest.class, + SDConfigServiceTest.class, StoreRestAddressUtilTest.class, }) @Slf4j diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/SDConfigServiceTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/SDConfigServiceTest.java new file mode 100644 index 0000000000..73e61295a1 --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/SDConfigServiceTest.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.pd.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.apache.hugegraph.pd.grpc.Metapb; +import org.junit.Test; + +public class SDConfigServiceTest { + + @Test + public void testStoreDiscoveryRequiresValidRestPort() { + assertNull(SDConfigService.getRestAddress( + store("127.0.0.1:8500", null))); + assertNull(SDConfigService.getRestAddress( + store("127.0.0.1:8500", "invalid"))); + assertEquals("127.0.0.1:8520", SDConfigService.getRestAddress( + store("127.0.0.1:8500", "8520"))); + } + + private static Metapb.Store store(String address, String restPort) { + Metapb.Store.Builder builder = Metapb.Store.newBuilder() + .setAddress(address); + if (restPort != null) { + builder.addLabels(Metapb.StoreLabel.newBuilder() + .setKey("rest.port") + .setValue(restPort)); + } + return builder.build(); + } +} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java index 0568b2e6a9..117da8e9ae 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java @@ -82,7 +82,7 @@ public String create(@Context GraphManager manager, static Id createScopedAccess(AuthManager authManager, String graphSpace, HugeAccess access) { - GraphSpaceGroupAPI.checkScopedGroupReference( + GraphSpaceGroupAPI.requireScopedGroupReference( authManager, graphSpace, access.source()); HugeTarget target; try { @@ -120,7 +120,7 @@ public String update(@Context GraphManager manager, throw new IllegalArgumentException("Invalid access id: " + id); } checkGraphSpace(graphSpace, access); - GraphSpaceGroupAPI.checkScopedGroupReference( + GraphSpaceGroupAPI.requireScopedGroupReference( manager.authManager(), graphSpace, access.source()); access = jsonAccess.build(access); manager.authManager().updateAccess(graphSpace, access); @@ -168,6 +168,10 @@ static List listScopedAccesses(AuthManager authManager, accesses = accesses.stream() .filter(access -> graphSpace.equals( access.graphSpace())) + .filter(access -> + GraphSpaceGroupAPI.isScopedGroupReference( + authManager, graphSpace, + access.source())) .collect(Collectors.toList()); return GraphSpaceGroupAPI.applyLimit(accesses, limit); } @@ -187,6 +191,8 @@ public String get(@Context GraphManager manager, HugeAccess access = manager.authManager().getAccess( graphSpace, UserAPI.parseId(id)); checkGraphSpace(graphSpace, access); + GraphSpaceGroupAPI.requireScopedGroupReference( + manager.authManager(), graphSpace, access.source()); return manager.serializer().writeAuthElement(access); } @@ -206,6 +212,8 @@ public void delete(@Context GraphManager manager, HugeAccess access = manager.authManager().getAccess( graphSpace, UserAPI.parseId(id)); checkGraphSpace(graphSpace, access); + GraphSpaceGroupAPI.requireScopedGroupReference( + manager.authManager(), graphSpace, access.source()); manager.authManager().deleteAccess(graphSpace, UserAPI.parseId(id)); } catch (NotFoundException e) { diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java index 15d0ea653e..d41de962e2 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java @@ -197,14 +197,16 @@ static void ensureManager(GraphManager manager, String graphSpace) { static void ensureAuthManager(GraphManager manager, String graphSpace) { E.checkArgument(manager.graphSpace(graphSpace) != null, "The graph space '%s' does not exist", graphSpace); - checkManagerPermission(manager.authManager(), graphSpace, - HugeGraphAuthProxy.username()); + if (manager.authManager().supportsGraphSpaceAuth()) { + checkManagerPermission(manager.authManager(), graphSpace, + HugeGraphAuthProxy.username()); + } } static void checkBelongReferences(AuthManager authManager, String graphSpace, HugeBelong belong) { - if (!(authManager instanceof StandardAuthManagerV2)) { + if (!authManager.supportsGraphSpaceAuth()) { return; } HugeUser user = authManager.findUser(belong.source().asString()); @@ -225,7 +227,7 @@ static void checkBelongReferences(AuthManager authManager, static void checkScopedGroupReference(AuthManager authManager, String graphSpace, Id groupId) { - if (!(authManager instanceof StandardAuthManagerV2)) { + if (!authManager.supportsGraphSpaceAuth()) { return; } HugeGroup group; @@ -239,6 +241,35 @@ static void checkScopedGroupReference(AuthManager authManager, } } + static void requireScopedGroupReference(AuthManager authManager, + String graphSpace, Id groupId) { + if (!authManager.supportsGraphSpaceAuth()) { + return; + } + HugeGroup group; + try { + group = authManager.getGroup(groupId); + } catch (NotFoundException e) { + throw new ForbiddenException( + "Permission denied: access group is not a business group"); + } + if (group == null) { + throw new ForbiddenException( + "Permission denied: access group is not a business group"); + } + checkScopedGroup(graphSpace, group); + } + + static boolean isScopedGroupReference(AuthManager authManager, + String graphSpace, Id groupId) { + try { + requireScopedGroupReference(authManager, graphSpace, groupId); + return true; + } catch (ForbiddenException e) { + return false; + } + } + static HugeGroup getGroup(AuthManager authManager, String id) { HugeGroup group; try { @@ -260,7 +291,9 @@ private static boolean isScopedGroup(String graphSpace, static class JsonGroup implements Checkable { @JsonProperty("group_name") - @Schema(description = "The name of group", required = true) + @Schema(description = "A required client label; the server generates " + + "the persisted scoped group name", + required = true) private String name; @JsonProperty("group_description") @Schema(description = "The description of group") @@ -287,7 +320,7 @@ HugeGroup build(String graphSpace) { @Override public void checkCreate(boolean isBatch) { E.checkArgumentNotNull(this.name, - "The name of group can't be null"); + "The group label can't be null"); } @Override diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java index e0a28d8ccd..4b0aed578f 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java @@ -1500,6 +1500,11 @@ public AuthManagerProxy(AuthManager origin) { this.authManager = origin; } + @Override + public boolean supportsGraphSpaceAuth() { + return this.authManager.supportsGraphSpaceAuth(); + } + private AuthElement updateCreator(AuthElement elem) { String username = currentUsername(); if (username != null && elem.creator() == null) { @@ -1610,6 +1615,12 @@ public HugeGroup deleteGroup(Id id) { return this.authManager.deleteGroup(id); } + @Override + public HugeGroup deleteGroup(String graphSpace, Id id) { + this.invalidRoleCache(); + return this.authManager.deleteGroup(graphSpace, id); + } + @Override public HugeGroup getGroup(Id id) { return this.authManager.getGroup(id); @@ -1633,6 +1644,14 @@ public Id createTarget(HugeTarget target) { return this.authManager.createTarget(target); } + @Override + public Id createTarget(String graphSpace, HugeTarget target) { + this.updateCreator(target); + verifyUserPermission(HugePermission.WRITE, target); + this.invalidRoleCache(); + return this.authManager.createTarget(graphSpace, target); + } + @Override public Id updateTarget(HugeTarget target) { this.updateCreator(target); @@ -1641,6 +1660,14 @@ public Id updateTarget(HugeTarget target) { return this.authManager.updateTarget(target); } + @Override + public Id updateTarget(String graphSpace, HugeTarget target) { + this.updateCreator(target); + verifyUserPermission(HugePermission.WRITE, target); + this.invalidRoleCache(); + return this.authManager.updateTarget(graphSpace, target); + } + @Override public HugeTarget deleteTarget(Id id) { verifyUserPermission(HugePermission.DELETE, @@ -1649,12 +1676,27 @@ public HugeTarget deleteTarget(Id id) { return this.authManager.deleteTarget(id); } + @Override + public HugeTarget deleteTarget(String graphSpace, Id id) { + verifyUserPermission(HugePermission.DELETE, + this.authManager.getTarget(graphSpace, id)); + this.invalidRoleCache(); + return this.authManager.deleteTarget(graphSpace, id); + } + @Override public HugeTarget getTarget(Id id) { return verifyUserPermission(HugePermission.READ, this.authManager.getTarget(id)); } + @Override + public HugeTarget getTarget(String graphSpace, Id id) { + return verifyUserPermission( + HugePermission.READ, + this.authManager.getTarget(graphSpace, id)); + } + @Override public List listTargets(List ids) { return verifyUserPermission(HugePermission.READ, @@ -1667,6 +1709,13 @@ public List listAllTargets(long limit) { this.authManager.listAllTargets(limit)); } + @Override + public List listAllTargets(String graphSpace, long limit) { + return verifyUserPermission( + HugePermission.READ, + this.authManager.listAllTargets(graphSpace, limit)); + } + @Override public Id createBelong(HugeBelong belong) { this.updateCreator(belong); @@ -1675,6 +1724,14 @@ public Id createBelong(HugeBelong belong) { return this.authManager.createBelong(belong); } + @Override + public Id createBelong(String graphSpace, HugeBelong belong) { + this.updateCreator(belong); + verifyUserPermission(HugePermission.WRITE, belong); + this.invalidRoleCache(); + return this.authManager.createBelong(graphSpace, belong); + } + @Override public Id updateBelong(HugeBelong belong) { this.updateCreator(belong); @@ -1683,6 +1740,14 @@ public Id updateBelong(HugeBelong belong) { return this.authManager.updateBelong(belong); } + @Override + public Id updateBelong(String graphSpace, HugeBelong belong) { + this.updateCreator(belong); + verifyUserPermission(HugePermission.WRITE, belong); + this.invalidRoleCache(); + return this.authManager.updateBelong(graphSpace, belong); + } + @Override public HugeBelong deleteBelong(Id id) { verifyUserPermission(HugePermission.DELETE, @@ -1691,12 +1756,27 @@ public HugeBelong deleteBelong(Id id) { return this.authManager.deleteBelong(id); } + @Override + public HugeBelong deleteBelong(String graphSpace, Id id) { + verifyUserPermission(HugePermission.DELETE, + this.authManager.getBelong(graphSpace, id)); + this.invalidRoleCache(); + return this.authManager.deleteBelong(graphSpace, id); + } + @Override public HugeBelong getBelong(Id id) { return verifyUserPermission(HugePermission.READ, this.authManager.getBelong(id)); } + @Override + public HugeBelong getBelong(String graphSpace, Id id) { + return verifyUserPermission( + HugePermission.READ, + this.authManager.getBelong(graphSpace, id)); + } + @Override public List listBelong(List ids) { return verifyUserPermission(HugePermission.READ, @@ -1709,12 +1789,27 @@ public List listAllBelong(long limit) { this.authManager.listAllBelong(limit)); } + @Override + public List listAllBelong(String graphSpace, long limit) { + return verifyUserPermission( + HugePermission.READ, + this.authManager.listAllBelong(graphSpace, limit)); + } + @Override public List listBelongByUser(Id user, long limit) { List r = this.authManager.listBelongByUser(user, limit); return verifyUserPermission(HugePermission.READ, r); } + @Override + public List listBelongByUser(String graphSpace, Id user, + long limit) { + List result = this.authManager.listBelongByUser( + graphSpace, user, limit); + return verifyUserPermission(HugePermission.READ, result); + } + @Override public List listBelongByGroup(Id group, long limit) { List r = this.authManager.listBelongByGroup(group, @@ -1722,6 +1817,14 @@ public List listBelongByGroup(Id group, long limit) { return verifyUserPermission(HugePermission.READ, r); } + @Override + public List listBelongByGroup(String graphSpace, Id group, + long limit) { + List result = this.authManager.listBelongByGroup( + graphSpace, group, limit); + return verifyUserPermission(HugePermission.READ, result); + } + @Override public Id createAccess(HugeAccess access) { this.updateCreator(access); @@ -1730,6 +1833,14 @@ public Id createAccess(HugeAccess access) { return this.authManager.createAccess(access); } + @Override + public Id createAccess(String graphSpace, HugeAccess access) { + this.updateCreator(access); + verifyUserPermission(HugePermission.WRITE, access); + this.invalidRoleCache(); + return this.authManager.createAccess(graphSpace, access); + } + @Override public Id updateAccess(HugeAccess access) { this.updateCreator(access); @@ -1738,6 +1849,14 @@ public Id updateAccess(HugeAccess access) { return this.authManager.updateAccess(access); } + @Override + public Id updateAccess(String graphSpace, HugeAccess access) { + this.updateCreator(access); + verifyUserPermission(HugePermission.WRITE, access); + this.invalidRoleCache(); + return this.authManager.updateAccess(graphSpace, access); + } + @Override public HugeAccess deleteAccess(Id id) { verifyUserPermission(HugePermission.DELETE, @@ -1746,12 +1865,27 @@ public HugeAccess deleteAccess(Id id) { return this.authManager.deleteAccess(id); } + @Override + public HugeAccess deleteAccess(String graphSpace, Id id) { + verifyUserPermission(HugePermission.DELETE, + this.authManager.getAccess(graphSpace, id)); + this.invalidRoleCache(); + return this.authManager.deleteAccess(graphSpace, id); + } + @Override public HugeAccess getAccess(Id id) { return verifyUserPermission(HugePermission.READ, this.authManager.getAccess(id)); } + @Override + public HugeAccess getAccess(String graphSpace, Id id) { + return verifyUserPermission( + HugePermission.READ, + this.authManager.getAccess(graphSpace, id)); + } + @Override public List listAccess(List ids) { return verifyUserPermission(HugePermission.READ, @@ -1764,6 +1898,13 @@ public List listAllAccess(long limit) { this.authManager.listAllAccess(limit)); } + @Override + public List listAllAccess(String graphSpace, long limit) { + return verifyUserPermission( + HugePermission.READ, + this.authManager.listAllAccess(graphSpace, limit)); + } + @Override public List listAccessByGroup(Id group, long limit) { List r = this.authManager.listAccessByGroup(group, @@ -1771,6 +1912,14 @@ public List listAccessByGroup(Id group, long limit) { return verifyUserPermission(HugePermission.READ, r); } + @Override + public List listAccessByGroup(String graphSpace, Id group, + long limit) { + List result = this.authManager.listAccessByGroup( + graphSpace, group, limit); + return verifyUserPermission(HugePermission.READ, result); + } + @Override public List listAccessByTarget(Id target, long limit) { List r = this.authManager.listAccessByTarget(target, @@ -1778,6 +1927,14 @@ public List listAccessByTarget(Id target, long limit) { return verifyUserPermission(HugePermission.READ, r); } + @Override + public List listAccessByTarget(String graphSpace, Id target, + long limit) { + List result = this.authManager.listAccessByTarget( + graphSpace, target, limit); + return verifyUserPermission(HugePermission.READ, result); + } + @Override public Id createProject(HugeProject project) { this.updateCreator(project); @@ -2089,7 +2246,12 @@ public String loginUser(String username, String password, long expire) { @Override public void logoutUser(String token) { - this.authManager.logoutUser(token); + try { + this.authManager.logoutUser(token); + } finally { + HugeGraphAuthProxy.this.usersRoleCache.invalidate( + IdGenerator.of(token)); + } } private void switchAuthManager(AuthManager authManager) { diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java index 388d61dd68..304e712fd5 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java @@ -55,9 +55,6 @@ public class WsAndHttpBasicAuthHandler extends SaslAuthenticationHandler { private static final String AUTHENTICATOR = "authenticator"; private static final String HTTP_AUTH = "http-authentication"; - private static final String BASIC_AUTH_PREFIX = "Basic "; - private static final String BEARER_TOKEN_PREFIX = "Bearer "; - public WsAndHttpBasicAuthHandler(Authenticator authenticator, Settings settings) { super(authenticator, settings); @@ -109,18 +106,18 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { final String header = request.headers().get("Authorization"); final Map credentials = new HashMap<>(); - if (header.startsWith(BASIC_AUTH_PREFIX)) { - if (!parseBasicCredentials(header, credentials)) { - sendError(ctx, msg); - return; - } - } else if (header.startsWith(BEARER_TOKEN_PREFIX)) { - String token = header.substring(BEARER_TOKEN_PREFIX.length()); - if (token.isEmpty()) { + int separator = header.indexOf(' '); + if (separator <= 0 || separator == header.length() - 1) { + sendError(ctx, msg); + return; + } + String scheme = header.substring(0, separator); + String payload = header.substring(separator + 1); + if ("Basic".equalsIgnoreCase(scheme)) { + if (!parseBasicCredentials(payload, credentials)) { sendError(ctx, msg); return; } - credentials.put(HugeAuthenticator.KEY_TOKEN, token); } else { sendError(ctx, msg); return; @@ -144,11 +141,10 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } } - private boolean parseBasicCredentials(String header, + private boolean parseBasicCredentials(String encoded, Map credentials) { byte[] userPass; try { - String encoded = header.substring(BASIC_AUTH_PREFIX.length()); userPass = this.decoder.decode(encoded); } catch (IllegalArgumentException e) { return false; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java index e9ffabe7eb..d1c8887238 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java @@ -29,6 +29,10 @@ public interface AuthManager { + default boolean supportsGraphSpaceAuth() { + return false; + } + void init(); boolean close(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java index b05969c53f..02f5e9b385 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java @@ -165,6 +165,8 @@ private void invalidatePasswordCache(Id id) { } private void checkGraphSpace(String graphSpace) { + E.checkArgument(graphSpace != null, + "The graph space can't be null"); E.checkArgument(this.defaultGraphSpace.equals(graphSpace), "The standalone auth manager only supports graph " + "space '%s', but got '%s'", diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java index 0cdc08d8c3..1f34aa4593 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java @@ -58,6 +58,11 @@ //only use in pd mode public class StandardAuthManagerV2 implements AuthManager { + @Override + public boolean supportsGraphSpaceAuth() { + return true; + } + public static final String ALL_GRAPHS = "*"; public static final String ALL_GRAPH_SPACES = "*"; public static final String DEFAULT_SETTER_ROLE_KEY = diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java index 8fd11f37b5..6333dbb44a 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java @@ -24,6 +24,7 @@ import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeAccess; import org.apache.hugegraph.auth.HugeBelong; +import org.apache.hugegraph.auth.HugeGroup; import org.apache.hugegraph.auth.HugePermission; import org.apache.hugegraph.auth.HugeTarget; import org.apache.hugegraph.backend.id.IdGenerator; @@ -123,6 +124,44 @@ public void testCreateAccessRejectsForeignTargetWithoutSideEffects() Mockito.any(HugeAccess.class)); } + @Test + public void testCreateAccessRejectsBuiltInRoleGroup() throws Exception { + AuthManager auth = Mockito.mock(AuthManager.class); + Mockito.when(auth.supportsGraphSpaceAuth()).thenReturn(true); + AccessAPI.JsonAccess jsonAccess = new ObjectMapper().readValue( + "{\"group\":\"builtin\",\"target\":\"target\"," + + "\"access_permission\":\"READ\"}", + AccessAPI.JsonAccess.class); + HugeAccess access = jsonAccess.build("SPACE_A"); + Mockito.when(auth.getGroup(access.source())).thenReturn(null); + + Assert.assertThrows(ForbiddenException.class, () -> + AccessAPI.createScopedAccess(auth, "SPACE_A", access)); + + Mockito.verify(auth, Mockito.never()) + .createAccess(Mockito.anyString(), + Mockito.any(HugeAccess.class)); + } + + @Test + public void testAccessListHidesBuiltInRoleAccesses() { + AuthManager auth = Mockito.mock(AuthManager.class); + Mockito.when(auth.supportsGraphSpaceAuth()).thenReturn(true); + HugeAccess builtIn = access("SPACE_A", "builtin"); + HugeAccess business = access("SPACE_A", "business"); + Mockito.when(auth.listAllAccess("SPACE_A", -1L)).thenReturn( + Arrays.asList(builtIn, business)); + Mockito.when(auth.getGroup(builtIn.source())).thenReturn(null); + Mockito.when(auth.getGroup(business.source())).thenReturn( + new HugeGroup(GraphSpaceGroupAPI.scopedPrefix("SPACE_A") + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + + List accesses = AccessAPI.listScopedAccesses( + auth, "SPACE_A", null, null, 100L); + + Assert.assertEquals(Arrays.asList(business), accesses); + } + @Test public void testBelongRejectsForeignGraphSpace() { HugeBelong belong = new HugeBelong("SPACE_A", diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java index 9aa0859fc0..bc0c832996 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java @@ -43,33 +43,25 @@ public class WsAndHttpBasicAuthHandlerTest { @SuppressWarnings("unchecked") @Test - public void testBearerTokenAuthenticatesHttpGremlinRequest() + public void testBasicCredentialsStillAuthenticateHttpGremlinRequest() throws Exception { Authenticator authenticator = Mockito.mock(Authenticator.class); AuthenticatedUser user = new AuthenticatedUser("admin"); Mockito.when(authenticator.authenticate(Mockito.anyMap())) .thenReturn(user); - WsAndHttpBasicAuthHandler handler = - new WsAndHttpBasicAuthHandler(authenticator, new Settings()); - EmbeddedChannel channel = new EmbeddedChannel(); - channel.pipeline().addLast("authenticator", handler); + EmbeddedChannel channel = channel(authenticator); + String encoded = Base64.getEncoder().encodeToString( + "admin:password".getBytes(StandardCharsets.UTF_8)); - DefaultFullHttpRequest request = - new DefaultFullHttpRequest(HTTP_1_1, POST, "/gremlin"); - request.headers().set(AUTHORIZATION, "Bearer server-token"); - channel.writeInbound(request); + channel.writeInbound(request("Basic " + encoded)); ArgumentCaptor> credentials = ArgumentCaptor.forClass(Map.class); Mockito.verify(authenticator).authenticate(credentials.capture()); - Mockito.verifyNoMoreInteractions(authenticator); - Assert.assertEquals("server-token", - credentials.getValue().get( - HugeAuthenticator.KEY_TOKEN)); - Assert.assertFalse( - credentials.getValue().containsKey("username")); - Assert.assertFalse( - credentials.getValue().containsKey("password")); + Assert.assertEquals("admin", credentials.getValue().get("username")); + Assert.assertEquals("password", credentials.getValue().get("password")); + Assert.assertFalse(credentials.getValue().containsKey( + HugeAuthenticator.KEY_TOKEN)); Assert.assertSame(user, channel.attr(StateKey.AUTHENTICATED_USER).get()); channel.finishAndReleaseAll(); @@ -77,27 +69,21 @@ public void testBearerTokenAuthenticatesHttpGremlinRequest() @SuppressWarnings("unchecked") @Test - public void testBasicCredentialsStillAuthenticateHttpGremlinRequest() - throws Exception { + public void testAuthorizationSchemeIsCaseInsensitive() throws Exception { Authenticator authenticator = Mockito.mock(Authenticator.class); - AuthenticatedUser user = new AuthenticatedUser("admin"); Mockito.when(authenticator.authenticate(Mockito.anyMap())) - .thenReturn(user); + .thenReturn(new AuthenticatedUser("admin")); EmbeddedChannel channel = channel(authenticator); + String encoded = Base64.getEncoder().encodeToString( "admin:password".getBytes(StandardCharsets.UTF_8)); - - channel.writeInbound(request("Basic " + encoded)); + channel.writeInbound(request("basic " + encoded)); ArgumentCaptor> credentials = ArgumentCaptor.forClass(Map.class); Mockito.verify(authenticator).authenticate(credentials.capture()); Assert.assertEquals("admin", credentials.getValue().get("username")); Assert.assertEquals("password", credentials.getValue().get("password")); - Assert.assertFalse(credentials.getValue().containsKey( - HugeAuthenticator.KEY_TOKEN)); - Assert.assertSame(user, - channel.attr(StateKey.AUTHENTICATED_USER).get()); channel.finishAndReleaseAll(); } @@ -147,8 +133,8 @@ public void testBasicCredentialsUseStandardBase64Decoder() } @Test - public void testEmptyBearerTokenIsRejected() throws Exception { - assertUnauthorized("Bearer "); + public void testBearerTokenIsRejected() throws Exception { + assertUnauthorized("Bearer server-token"); } @Test diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java index 3de789e63b..1b209c9139 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java @@ -256,6 +256,72 @@ public void testDefaultRoleMutationInvalidatesUserRoleCache() .validateUser("cache_user", "pass"); } + @Test + public void testLogoutInvalidatesTokenRoleCache() throws Exception { + HugeGraph graph = Mockito.mock(HugeGraph.class); + HugeConfig config = Mockito.mock(HugeConfig.class); + AuthManager authManager = Mockito.mock(AuthManager.class); + TaskScheduler scheduler = Mockito.mock(TaskScheduler.class); + String token = "cached-token"; + + Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph"); + Mockito.when(graph.configuration()).thenReturn(config); + Mockito.when(graph.authManager()).thenReturn(authManager); + Mockito.when(graph.taskScheduler()).thenReturn(scheduler); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_EXPIRE)) + .thenReturn(3600L); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_CAPACITY)) + .thenReturn(100L); + Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE)) + .thenReturn(1000D); + Mockito.when(authManager.validateUser(token)) + .thenReturn(new UserWithRole("cache_user")); + + AuthManager proxyAuthManager = + new HugeGraphAuthProxy(graph).authManager(); + proxyAuthManager.validateUser(token); + proxyAuthManager.validateUser(token); + Mockito.verify(authManager, Mockito.times(1)).validateUser(token); + + proxyAuthManager.logoutUser(token); + proxyAuthManager.validateUser(token); + + Mockito.verify(authManager).logoutUser(token); + Mockito.verify(authManager, Mockito.times(2)).validateUser(token); + } + + @Test + public void testProxyOverridesEveryScopedDefaultMethod() throws Exception { + HugeGraph graph = Mockito.mock(HugeGraph.class); + HugeConfig config = Mockito.mock(HugeConfig.class); + AuthManager origin = Mockito.mock(AuthManager.class); + TaskScheduler scheduler = Mockito.mock(TaskScheduler.class); + + Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph"); + Mockito.when(graph.configuration()).thenReturn(config); + Mockito.when(graph.authManager()).thenReturn(origin); + Mockito.when(graph.taskScheduler()).thenReturn(scheduler); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_EXPIRE)) + .thenReturn(3600L); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_CAPACITY)) + .thenReturn(100L); + Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE)) + .thenReturn(1000D); + Mockito.when(origin.supportsGraphSpaceAuth()).thenReturn(true); + + AuthManager proxy = new HugeGraphAuthProxy(graph).authManager(); + Assert.assertTrue(proxy.supportsGraphSpaceAuth()); + for (Method method : AuthManager.class.getMethods()) { + Class[] parameters = method.getParameterTypes(); + if (!method.isDefault() || parameters.length == 0 || + parameters[0] != String.class) { + continue; + } + Assert.assertNotNull(proxy.getClass().getDeclaredMethod( + method.getName(), parameters)); + } + } + @Test public void testValidateUserDoesNotLogBearerToken() { String token = "secret-proxy-bearer-token"; From 87fcddc61d6c99cc59c3cd34e0f8022e1aa001b5 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 19 Jul 2026 17:05:23 +0800 Subject: [PATCH 15/15] fix(pd): restore reactor test compilation - call Store REST address utility directly from discovery - remove the service test hidden by the repackaged jar - retain Store address validation in the existing utility suite --- .../hugegraph/pd/service/SDConfigService.java | 6 +-- .../hugegraph/pd/rest/PDRestSuiteTest.java | 2 - .../pd/service/SDConfigServiceTest.java | 48 ------------------- 3 files changed, 1 insertion(+), 55 deletions(-) delete mode 100644 hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/SDConfigServiceTest.java diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java index d627d54021..a5d7cc1252 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/SDConfigService.java @@ -206,7 +206,7 @@ private Set getStoreAddresses() { } if (stores != null) { stores.stream().forEach(e -> { - String buf = getRestAddress(e); + String buf = StoreRestAddressUtil.getRestAddress(e); if (buf != null) { res.add(buf); } @@ -215,10 +215,6 @@ private Set getStoreAddresses() { return res; } - static String getRestAddress(Metapb.Store store) { - return StoreRestAddressUtil.getRestAddress(store); - } - public List getConfigs(String appName, String path) { HgAssert.isArgumentNotNull(appName, "appName"); SDConfig config; diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java index 38646b4d91..5dba561948 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/rest/PDRestSuiteTest.java @@ -17,7 +17,6 @@ package org.apache.hugegraph.pd.rest; -import org.apache.hugegraph.pd.service.SDConfigServiceTest; import org.apache.hugegraph.pd.util.StoreRestAddressUtilTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -27,7 +26,6 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ RestApiTest.class, - SDConfigServiceTest.class, StoreRestAddressUtilTest.class, }) @Slf4j diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/SDConfigServiceTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/SDConfigServiceTest.java deleted file mode 100644 index 73e61295a1..0000000000 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/SDConfigServiceTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.pd.service; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import org.apache.hugegraph.pd.grpc.Metapb; -import org.junit.Test; - -public class SDConfigServiceTest { - - @Test - public void testStoreDiscoveryRequiresValidRestPort() { - assertNull(SDConfigService.getRestAddress( - store("127.0.0.1:8500", null))); - assertNull(SDConfigService.getRestAddress( - store("127.0.0.1:8500", "invalid"))); - assertEquals("127.0.0.1:8520", SDConfigService.getRestAddress( - store("127.0.0.1:8500", "8520"))); - } - - private static Metapb.Store store(String address, String restPort) { - Metapb.Store.Builder builder = Metapb.Store.newBuilder() - .setAddress(address); - if (restPort != null) { - builder.addLabels(Metapb.StoreLabel.newBuilder() - .setKey("rest.port") - .setValue(restPort)); - } - return builder.build(); - } -}