diff --git a/CHANGES.md b/CHANGES.md index d5f8c495..c5cb6fe4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Release Notes. * Bump up the API to support sharding_key. * Bump up the API to support version 0.9. * Support stage query on TopN. +* Support auth with username and password. 0.8.0 ------------------ diff --git a/README.md b/README.md index af6c62ad..b8161b98 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ options are listed below, | forceReconnectionThreshold | Threshold of force gRPC reconnection if network issue is encountered | 1 | | forceTLS | Force use TLS for gRPC | false | | sslTrustCAPath | SSL: Trusted CA Path | | +| sslCertChainPath | SSL: Cert Chain Path, BanyanDB server not support mTLS yet | | +| sslKeyPath | SSL: Cert Key Path, BanyanDB server not support mTLS yet | | +| username | Basic Auth: username of BanyanDB server | | +| password | Basic Auth: password of BanyanDB server | | ## Schema Management diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClient.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClient.java index 50a9079c..7d3ddd03 100644 --- a/src/main/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClient.java +++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClient.java @@ -23,6 +23,7 @@ import com.google.common.base.Strings; import com.google.protobuf.Timestamp; import io.grpc.Channel; +import io.grpc.ClientInterceptors; import io.grpc.ManagedChannel; import io.grpc.Status; import io.grpc.stub.StreamObserver; @@ -51,6 +52,7 @@ import org.apache.skywalking.banyandb.measure.v1.MeasureServiceGrpc; import org.apache.skywalking.banyandb.stream.v1.BanyandbStream; import org.apache.skywalking.banyandb.stream.v1.StreamServiceGrpc; +import org.apache.skywalking.banyandb.v1.client.auth.AuthInterceptor; import org.apache.skywalking.banyandb.v1.client.grpc.HandleExceptionsWith; import org.apache.skywalking.banyandb.v1.client.grpc.channel.ChannelManager; import org.apache.skywalking.banyandb.v1.client.grpc.channel.DefaultChannelFactory; @@ -183,8 +185,18 @@ public void connect() throws IOException { for (int i = 0; i < this.targets.length; i++) { addresses[i] = URI.create("//" + this.targets[i]); } - this.channel = ChannelManager.create(this.options.buildChannelManagerSettings(), + Channel rawChannel = ChannelManager.create(this.options.buildChannelManagerSettings(), new DefaultChannelFactory(addresses, this.options)); + Channel interceptedChannel = rawChannel; + // register auth interceptor + String username = options.getUsername(); + String password = options.getPassword(); + if (!"".equals(username) && !"".equals(password)) { + interceptedChannel = ClientInterceptors.intercept(rawChannel, + new AuthInterceptor(username, password)); + } + // Ensure this.channel is assigned only once. + this.channel = interceptedChannel; streamServiceBlockingStub = StreamServiceGrpc.newBlockingStub(this.channel); measureServiceBlockingStub = MeasureServiceGrpc.newBlockingStub(this.channel); streamServiceStub = StreamServiceGrpc.newStub(this.channel); diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java index e22959c4..9be150a2 100644 --- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java +++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java @@ -63,6 +63,14 @@ public class Options { * SSL: Cert Key Path, BanyanDB server not support mTLS yet */ private String sslKeyPath = ""; + /** + * Basic Auth: username of BanyanDB server + */ + private String username = ""; + /** + * Basic Auth: password of BanyanDB server + */ + private String password = ""; public Options() { } diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/auth/AuthInterceptor.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/auth/AuthInterceptor.java new file mode 100644 index 00000000..85b31902 --- /dev/null +++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/auth/AuthInterceptor.java @@ -0,0 +1,60 @@ +/* + * 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.skywalking.banyandb.v1.client.auth; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; + +public class AuthInterceptor implements ClientInterceptor { + private final String username; + private final String password; + + private static final Metadata.Key USERNAME_KEY = + Metadata.Key.of("username", Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key PASSWORD_KEY = + Metadata.Key.of("password", Metadata.ASCII_STRING_MARSHALLER); + + public AuthInterceptor(String username, String password) { + this.username = username; + this.password = password; + } + + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + headers.put(USERNAME_KEY, username); + headers.put(PASSWORD_KEY, password); + + super.start(responseListener, headers); + } + }; + } +} \ No newline at end of file diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/ITBanyanDBAuthTest.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/ITBanyanDBAuthTest.java new file mode 100644 index 00000000..73c59d84 --- /dev/null +++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/ITBanyanDBAuthTest.java @@ -0,0 +1,107 @@ +/* + * 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.skywalking.banyandb.v1.client; + +import org.apache.skywalking.banyandb.common.v1.BanyandbCommon; +import org.apache.skywalking.banyandb.v1.client.grpc.exception.UnauthenticatedException; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertThrows; + +public class ITBanyanDBAuthTest { + private static final String REGISTRY = "ghcr.io"; + private static final String IMAGE_NAME = "apache/skywalking-banyandb"; + private static final String TAG = "42ec9df7457868926eb80157b36355d94fcd6bba"; + + private static final String IMAGE = REGISTRY + "/" + IMAGE_NAME + ":" + TAG; + + protected static final int GRPC_PORT = 17912; + protected static final int HTTP_PORT = 17913; + + @Rule + public GenericContainer banyanDB; + + public ITBanyanDBAuthTest() throws Exception { + // Step 1: prepare config file with 0600 permissions + Path tempConfigPath = Files.createTempFile("bydb_server_config", ".yaml"); + Files.write(tempConfigPath, Files.readAllBytes( + Paths.get(getClass().getClassLoader().getResource("config.yaml").toURI())) + ); + Files.setPosixFilePermissions(tempConfigPath, PosixFilePermissions.fromString("rw-------")); + + // Step 2: create container + banyanDB = new GenericContainer<>(DockerImageName.parse(IMAGE)) + .withCopyFileToContainer( + MountableFile.forHostPath(tempConfigPath), + "/tmp/bydb_server_config.yaml" + ) + .withCommand("standalone", + "--auth-config-file", "/tmp/bydb_server_config.yaml" + ) + .withExposedPorts(GRPC_PORT, HTTP_PORT) + .waitingFor(Wait.forHttp("/api/healthz").forPort(HTTP_PORT)); + } + + @Test + public void testAuthWithCorrect() throws IOException { + BanyanDBClient client = createClient("admin", "123456"); + client.connect(); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + // get api version + client.getAPIVersion(); + // list all groups + List groupList = client.findGroups(); + Assert.assertEquals(0, groupList.size()); + }); + client.close(); + } + + @Test + public void testAuthWithWrong() throws IOException { + BanyanDBClient client = createClient("admin", "123456" + "wrong"); + client.connect(); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + assertThrows(UnauthenticatedException.class, client::getAPIVersion); + }); + client.close(); + } + + private BanyanDBClient createClient(String username, String password) { + Options options = new Options(); + options.setUsername(username); + options.setPassword(password); + String url = String.format("%s:%d", banyanDB.getHost(), banyanDB.getMappedPort(GRPC_PORT)); + return new BanyanDBClient(new String[]{url}, options); + } +} \ No newline at end of file diff --git a/src/test/resources/config.yaml b/src/test/resources/config.yaml new file mode 100644 index 00000000..286d88a2 --- /dev/null +++ b/src/test/resources/config.yaml @@ -0,0 +1,20 @@ +# 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. + +users: + - username: admin + password: 123456 + - username: test + password: 123456 \ No newline at end of file