HTTP transport implementation
[netconf.git] / transport / transport-http / src / test / java / org / opendaylight / netconf / transport / http / BasicAuthHandlerTest.java
diff --git a/transport/transport-http/src/test/java/org/opendaylight/netconf/transport/http/BasicAuthHandlerTest.java b/transport/transport-http/src/test/java/org/opendaylight/netconf/transport/http/BasicAuthHandlerTest.java
new file mode 100644 (file)
index 0000000..aeb006b
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.http;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.opendaylight.netconf.transport.http.BasicAuthHandler.BASIC_AUTH_PREFIX;
+
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http.DefaultHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpVersion;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.stream.Stream;
+import org.apache.commons.codec.digest.Crypt;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.crypt.hash.rev140806.CryptHash;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.HttpServerGrouping;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.ClientAuthentication;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.ClientAuthenticationBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.client.authentication.users.UserBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.client.authentication.users.user.auth.type.basic.BasicBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.client.authentication.users.user.auth.type.basic.basic.PasswordBuilder;
+import org.opendaylight.yangtools.yang.binding.util.BindingMap;
+
+public class BasicAuthHandlerTest {
+    private static final String USERNAME1 = "username-1";
+    private static final String USERNAME2 = "username-2";
+    private static final String PASSWORD1 = "pa$$W0rd!1";
+    private static final String PASSWORD2 = "pa$$W0rd#2";
+    private static final String HASHED_PASSWORD2 = Crypt.crypt(PASSWORD2, "$6$rounds=4500$sha512salt");
+
+    private EmbeddedChannel channel;
+
+    @BeforeEach
+    void beforeEach() {
+        final var authHandler = BasicAuthHandler.ofNullable(new HttpServerGrouping() {
+            @Override
+            public Class<? extends HttpServerGrouping> implementedInterface() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public String getServerName() {
+                return null;
+            }
+
+            @Override
+            public ClientAuthentication getClientAuthentication() {
+                final var user1 = new UserBuilder()
+                    .setUserId(USERNAME1)
+                    .setAuthType(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208
+                            .http.server.grouping.client.authentication.users.user.auth.type.BasicBuilder()
+                        .setBasic(new BasicBuilder()
+                            .setUsername(USERNAME1)
+                            .setPassword(new PasswordBuilder()
+                                .setHashedPassword(new CryptHash("$0$" + PASSWORD1))
+                                .build())
+                            .build())
+                        .build())
+                    .build();
+                final var user2 = new UserBuilder()
+                    .setUserId(USERNAME2)
+                    .setAuthType(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208
+                            .http.server.grouping.client.authentication.users.user.auth.type.BasicBuilder()
+                        .setBasic(new BasicBuilder()
+                            .setUsername(USERNAME2)
+                            .setPassword(new PasswordBuilder()
+                                .setHashedPassword(new CryptHash(HASHED_PASSWORD2))
+                                .build())
+                            .build())
+                        .build())
+                    .build();
+
+                return new ClientAuthenticationBuilder()
+                    .setUsers(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208
+                        .http.server.grouping.client.authentication.UsersBuilder()
+                        .setUser(BindingMap.of(user1, user2)).build()).build();
+            }
+        });
+        assertNotNull(authHandler);
+
+        channel = new EmbeddedChannel(authHandler);
+    }
+
+    @ParameterizedTest(name = "BasicAuth success: {0} password configured")
+    @MethodSource("authSuccessArgs")
+    void authSuccess(final String testDesc, final String username, final String password) {
+        final String authHeader = authHeader(BASIC_AUTH_PREFIX, username, password);
+        final var request = newHttpRequest(authHeader);
+        channel.writeInbound(request);
+        // nonnull read indicates the message is passed for next handler
+        assertEquals(request, channel.readInbound());
+    }
+
+    private static Stream<Arguments> authSuccessArgs() {
+        return Stream.of(
+            // test descriptor, username, password
+            Arguments.of("unencrypted", USERNAME1, PASSWORD1),
+            Arguments.of("sha512 encrypted", USERNAME2, PASSWORD2));
+    }
+
+    @ParameterizedTest(name = "BasicAuth failure: {0}")
+    @MethodSource("authFailureArgs")
+    void authFailure(final String testDesc, final String authHeader) {
+        channel.writeInbound(newHttpRequest(authHeader));
+        // null indicates the request is consumed and not passed to next handler
+        assertNull(channel.readInbound());
+        // verify response
+        final var outbound = channel.readOutbound();
+        assertNotNull(outbound);
+        final var response = assertInstanceOf(HttpResponse.class, outbound);
+        assertEquals(HttpResponseStatus.UNAUTHORIZED, response.status());
+    }
+
+    private static Stream<Arguments> authFailureArgs() {
+        return Stream.of(
+            // test descriptor, auth header
+            Arguments.of("no Authorization header", null),
+            Arguments.of("Authorization header does not start with `Basic`", "Bearer ABCD+"),
+            Arguments.of("Base64 decode failure", BASIC_AUTH_PREFIX + "cannot-decode-this"),
+            Arguments.of("No expected username:password",
+                BASIC_AUTH_PREFIX + Base64.getEncoder().encodeToString("abcd".getBytes(StandardCharsets.UTF_8))),
+            Arguments.of("Unknown user", authHeader(BASIC_AUTH_PREFIX, "unknown", "user")),
+            Arguments.of("Wrong password", authHeader(BASIC_AUTH_PREFIX, USERNAME1, PASSWORD2)));
+    }
+
+    private static String authHeader(final String prefix, final String username, final String password) {
+        return prefix + Base64.getEncoder()
+            .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static HttpRequest newHttpRequest(final String authHeader) {
+        final var request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/uri");
+        if (authHeader != null) {
+            request.headers().add(HttpHeaderNames.AUTHORIZATION, authHeader);
+        }
+        return request;
+    }
+}