From 225f156864c3dd1d07436acca48e1af5b83de2ed Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Wed, 15 Jan 2025 00:22:29 +0800 Subject: [PATCH] added anonymous user --- .../net/woggioni/gbcs/benchmark/Main.java | 118 ++++++++-- .../main/kotlin/net/woggioni/gbcs/base/Xml.kt | 41 ++-- .../cli/impl/commands/PasswordHashCommand.kt | 9 +- .../net/woggioni/gbcs/cli/logback.xml | 3 +- .../gbcs/memcached/MemcachedCacheProvider.kt | 28 ++- .../gbcs/memcached/schema/gbcs-memcached.xsd | 4 +- .../woggioni/gbcs/GradleBuildCacheServer.kt | 39 ++-- .../gbcs/auth/ClientCertificateValidator.kt | 16 +- .../net/woggioni/gbcs/configuration/Parser.kt | 43 ++-- .../woggioni/gbcs/configuration/Serializer.kt | 30 ++- .../net/woggioni/gbcs/schema/gbcs.xsd | 4 +- .../gbcs/test/AbstractBasicAuthServerTest.kt | 76 ++++++ .../gbcs/test/AbstractTlsServerTest.kt | 192 ++++++++++++++++ .../woggioni/gbcs/test/BasicAuthServerTest.kt | 94 ++------ .../woggioni/gbcs/test/ConfigurationTest.kt | 3 +- .../NoAnonymousUserBasicAuthServerTest.kt | 53 +++++ .../gbcs/test/NoAnonymousUserTlsServerTest.kt | 47 ++++ .../net/woggioni/gbcs/test/TlsServerTest.kt | 217 +++--------------- .../net/woggioni/gbcs/test/gbcs-memcached.xml | 2 +- .../net/woggioni/gbcs/test/gbcs-tls.xml | 53 +++++ 20 files changed, 713 insertions(+), 359 deletions(-) create mode 100644 src/test/kotlin/net/woggioni/gbcs/test/AbstractBasicAuthServerTest.kt create mode 100644 src/test/kotlin/net/woggioni/gbcs/test/AbstractTlsServerTest.kt create mode 100644 src/test/kotlin/net/woggioni/gbcs/test/NoAnonymousUserBasicAuthServerTest.kt create mode 100644 src/test/kotlin/net/woggioni/gbcs/test/NoAnonymousUserTlsServerTest.kt create mode 100644 src/test/resources/net/woggioni/gbcs/test/gbcs-tls.xml diff --git a/benchmark/src/jmh/java/net/woggioni/gbcs/benchmark/Main.java b/benchmark/src/jmh/java/net/woggioni/gbcs/benchmark/Main.java index b283ee6..ee58d3c 100644 --- a/benchmark/src/jmh/java/net/woggioni/gbcs/benchmark/Main.java +++ b/benchmark/src/jmh/java/net/woggioni/gbcs/benchmark/Main.java @@ -2,6 +2,7 @@ package net.woggioni.gbcs.benchmark; import lombok.Getter; import lombok.SneakyThrows; +import net.woggioni.jwo.Fun; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Level; @@ -10,12 +11,20 @@ import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; import java.util.Arrays; import java.util.Base64; import java.util.Collections; @@ -46,10 +55,17 @@ public class Main { private final Random random = new Random(101325); @Getter - private final HttpClient client = HttpClient.newHttpClient(); + private final HttpClient client = createHttpClient(); private final Map entries = new HashMap<>(); + + private HttpClient createHttpClient() { + final var clientBuilder = HttpClient.newBuilder(); + getSslContext().ifPresent(clientBuilder::sslContext); + return clientBuilder.build(); + } + public final Map getEntries() { return Collections.unmodifiableMap(entries); } @@ -79,6 +95,57 @@ public class Main { return new URI(properties.getProperty("gbcs.server.url")); } + @SneakyThrows + public Optional getClientTrustStorePassword() { + return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.truststore.password")) + .filter(Predicate.not(String::isEmpty)); + + } + + @SneakyThrows + public Optional getClientTrustStore() { + return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.truststore.file")) + .filter(Predicate.not(String::isEmpty)) + .map(Path::of) + .map((Fun) keyStoreFile -> { + final var keyStore = KeyStore.getInstance("PKCS12"); + try (final var is = Files.newInputStream(keyStoreFile)) { + keyStore.load(is, getClientTrustStorePassword().map(String::toCharArray).orElse(null)); + } + return keyStore; + }); + + } + + @SneakyThrows + public Optional getClientKeyStore() { + return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.keystore.file")) + .filter(Predicate.not(String::isEmpty)) + .map(Path::of) + .map((Fun) keyStoreFile -> { + final var keyStore = KeyStore.getInstance("PKCS12"); + try (final var is = Files.newInputStream(keyStoreFile)) { + keyStore.load(is, getClientKeyStorePassword().map(String::toCharArray).orElse(null)); + } + return keyStore; + }); + + } + + @SneakyThrows + public Optional getClientKeyStorePassword() { + return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.keystore.password")) + .filter(Predicate.not(String::isEmpty)); + + } + + @SneakyThrows + public Optional getClientKeyPassword() { + return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.key.password")) + .filter(Predicate.not(String::isEmpty)); + + } + @SneakyThrows public String getUser() { return Optional.ofNullable(properties.getProperty("gbcs.server.username")) @@ -99,26 +166,51 @@ public class Main { return "Basic " + new String(b64); } + @SneakyThrows + private Optional getSslContext() { + return getClientKeyStore().map((Fun) clientKeyStore -> { + final var kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(clientKeyStore, getClientKeyStorePassword().map(String::toCharArray).orElse(null)); + + + // Set up trust manager factory with the truststore + final var trustManagers = getClientTrustStore().map((Fun) ts -> { + final var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); + return tmf.getTrustManagers(); + }).orElse(new TrustManager[0]); + + // Create SSL context with the key and trust managers + final var sslContext = SSLContext.getInstance("TLS"); + sslContext.init(kmf.getKeyManagers(), trustManagers, null); + return sslContext; + }); + } @SneakyThrows @Setup(Level.Trial) public void setUp() { - try (final var client = HttpClient.newHttpClient()) { - for (int i = 0; i < 10000; i++) { - final var pair = newEntry(); - final var requestBuilder = newRequestBuilder(pair.getKey()) - .header("Content-Type", "application/octet-stream") - .PUT(HttpRequest.BodyPublishers.ofByteArray(pair.getValue())); - final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); - if (201 != response.statusCode()) { - throw new IllegalStateException(Integer.toString(response.statusCode())); - } else { - entries.put(pair.getKey(), pair.getValue()); - } + final var client = getClient(); + for (int i = 0; i < 1000; i++) { + final var pair = newEntry(); + final var requestBuilder = newRequestBuilder(pair.getKey()) + .header("Content-Type", "application/octet-stream") + .PUT(HttpRequest.BodyPublishers.ofByteArray(pair.getValue())); + final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + if (201 != response.statusCode()) { + throw new IllegalStateException(Integer.toString(response.statusCode())); + } else { + entries.put(pair.getKey(), pair.getValue()); } } } + @TearDown + public void tearDown() { + client.close(); + } + + private Iterator> it = null; private Map.Entry nextEntry() { diff --git a/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/Xml.kt b/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/Xml.kt index a2622f8..6e61248 100644 --- a/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/Xml.kt +++ b/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/Xml.kt @@ -146,29 +146,29 @@ class Xml(val doc: Document, val element: Element) { dbf.isExpandEntityReferences = true dbf.isIgnoringComments = true dbf.isNamespaceAware = true - dbf.isValidating = false - dbf.setFeature("http://apache.org/xml/features/validation/schema", true); + dbf.isValidating = schemaResourceURL == null + dbf.setFeature("http://apache.org/xml/features/validation/schema", true) schemaResourceURL?.let { dbf.schema = getSchema(it) } return dbf } - fun newDocumentBuilder(resource: URL, schemaResourceURL: URL?): DocumentBuilder { - val db = newDocumentBuilderFactory(schemaResourceURL).newDocumentBuilder() - db.setErrorHandler(ErrorHandler(resource)) - return db - } + fun newDocumentBuilder(resource: URL, schemaResourceURL: URL?): DocumentBuilder { + val db = newDocumentBuilderFactory(schemaResourceURL).newDocumentBuilder() + db.setErrorHandler(ErrorHandler(resource)) + return db + } - fun parseXmlResource(resource: URL, schemaResourceURL: URL?): Document { - val db = newDocumentBuilder(resource, schemaResourceURL) - return resource.openStream().use(db::parse) - } + fun parseXmlResource(resource: URL, schemaResourceURL: URL?): Document { + val db = newDocumentBuilder(resource, schemaResourceURL) + return resource.openStream().use(db::parse) + } - fun parseXml(sourceURL : URL, sourceStream: InputStream? = null, schemaResourceURL: URL? = null): Document { - val db = newDocumentBuilder(sourceURL, schemaResourceURL) - return sourceStream?.let(db::parse) ?: sourceURL.openStream().use(db::parse) - } + fun parseXml(sourceURL: URL, sourceStream: InputStream? = null, schemaResourceURL: URL? = null): Document { + val db = newDocumentBuilder(sourceURL, schemaResourceURL) + return sourceStream?.let(db::parse) ?: sourceURL.openStream().use(db::parse) + } fun write(doc: Document, output: OutputStream) { val transformerFactory = TransformerFactory.newInstance() @@ -183,7 +183,12 @@ class Xml(val doc: Document, val element: Element) { transformer.transform(source, result) } - fun of(namespaceURI: String, qualifiedName: String, schemaResourceURL: URL? = null, cb: Xml.(el: Element) -> Unit): Document { + fun of( + namespaceURI: String, + qualifiedName: String, + schemaResourceURL: URL? = null, + cb: Xml.(el: Element) -> Unit + ): Document { val dbf = newDocumentBuilderFactory(schemaResourceURL) val db = dbf.newDocumentBuilder() val doc = db.newDocument() @@ -207,7 +212,7 @@ class Xml(val doc: Document, val element: Element) { fun node( name: String, - namespaceURI : String? = null, + namespaceURI: String? = null, attrs: Map = emptyMap(), cb: Xml.(el: Element) -> Unit = {} ): Element { @@ -222,7 +227,7 @@ class Xml(val doc: Document, val element: Element) { } } - fun attr(key: String, value: String, namespaceURI : String? = null) { + fun attr(key: String, value: String, namespaceURI: String? = null) { element.setAttributeNS(namespaceURI, key, value) } diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/PasswordHashCommand.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/PasswordHashCommand.kt index 7a39f8c..46247a2 100644 --- a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/PasswordHashCommand.kt +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/PasswordHashCommand.kt @@ -5,9 +5,9 @@ import net.woggioni.gbcs.cli.impl.GbcsCommand import net.woggioni.gbcs.cli.impl.converters.OutputStreamConverter import net.woggioni.jwo.UncloseableOutputStream import picocli.CommandLine -import java.io.BufferedWriter import java.io.OutputStream import java.io.OutputStreamWriter +import java.io.PrintWriter @CommandLine.Command( @@ -20,7 +20,7 @@ class PasswordHashCommand : GbcsCommand() { names = ["-o", "--output-file"], description = ["Write the output to a file instead of stdout"], converter = [OutputStreamConverter::class], - defaultValue = "stdout", + showDefaultValue = CommandLine.Help.Visibility.NEVER, paramLabel = "OUTPUT_FILE" ) private var outputStream: OutputStream = UncloseableOutputStream(System.out) @@ -30,9 +30,8 @@ class PasswordHashCommand : GbcsCommand() { val password2 = String(System.console().readPassword("Type your password again for confirmation:")) if(password1 != password2) throw IllegalArgumentException("Passwords do not match") - BufferedWriter(OutputStreamWriter(outputStream, Charsets.UTF_8)).use { - it.write(hashPassword(password1)) - it.newLine() + PrintWriter(OutputStreamWriter(outputStream, Charsets.UTF_8)).use { + it.println(hashPassword(password1)) } } } \ No newline at end of file diff --git a/gbcs-cli/src/main/resources/net/woggioni/gbcs/cli/logback.xml b/gbcs-cli/src/main/resources/net/woggioni/gbcs/cli/logback.xml index 76a457a..217d42e 100644 --- a/gbcs-cli/src/main/resources/net/woggioni/gbcs/cli/logback.xml +++ b/gbcs-cli/src/main/resources/net/woggioni/gbcs/cli/logback.xml @@ -12,10 +12,11 @@ - + + \ No newline at end of file diff --git a/gbcs-memcached/src/main/kotlin/net/woggioni/gbcs/memcached/MemcachedCacheProvider.kt b/gbcs-memcached/src/main/kotlin/net/woggioni/gbcs/memcached/MemcachedCacheProvider.kt index ed82409..f82659d 100644 --- a/gbcs-memcached/src/main/kotlin/net/woggioni/gbcs/memcached/MemcachedCacheProvider.kt +++ b/gbcs-memcached/src/main/kotlin/net/woggioni/gbcs/memcached/MemcachedCacheProvider.kt @@ -9,14 +9,16 @@ import net.woggioni.gbcs.base.Xml.Companion.asIterable import org.w3c.dom.Document import org.w3c.dom.Element import java.time.Duration -import java.util.zip.Deflater class MemcachedCacheProvider : CacheProvider { override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd" override fun getXmlType() = "memcachedCacheType" - override fun getXmlNamespace()= "urn:net.woggioni.gbcs-memcached" + override fun getXmlNamespace() = "urn:net.woggioni.gbcs-memcached" + + val xmlNamespacePrefix : String + get() = "gbcs-memcached" override fun deserialize(el: Element): MemcachedCacheConfiguration { val servers = mutableListOf() @@ -35,7 +37,7 @@ class MemcachedCacheProvider : CacheProvider { val compressionMode = el.getAttribute("compression-mode") .takeIf(String::isNotEmpty) ?.let { - when(it) { + when (it) { "gzip" -> CompressionMode.GZIP "zip" -> CompressionMode.ZIP else -> CompressionMode.ZIP @@ -60,12 +62,14 @@ class MemcachedCacheProvider : CacheProvider { ) } - override fun serialize(doc: Document, cache : MemcachedCacheConfiguration) = cache.run { - val result = doc.createElementNS(xmlNamespace,"cache") + override fun serialize(doc: Document, cache: MemcachedCacheConfiguration) = cache.run { + val result = doc.createElement("cache") Xml.of(doc, result) { - attr("xs:type", xmlType, GBCS.XML_SCHEMA_NAMESPACE_URI) + attr("xmlns:${xmlNamespacePrefix}", xmlNamespace, namespaceURI = "http://www.w3.org/2000/xmlns/") + + attr("xs:type", "${xmlNamespacePrefix}:$xmlType", GBCS.XML_SCHEMA_NAMESPACE_URI) for (server in servers) { - node("server", xmlNamespace) { + node("server") { attr("host", server.host) attr("port", server.port.toString()) } @@ -75,10 +79,12 @@ class MemcachedCacheProvider : CacheProvider { digestAlgorithm?.let { digestAlgorithm -> attr("digest", digestAlgorithm) } - attr("compression-mode", when(compressionMode) { - CompressionMode.GZIP -> "gzip" - CompressionMode.ZIP -> "zip" - }) + attr( + "compression-mode", when (compressionMode) { + CompressionMode.GZIP -> "gzip" + CompressionMode.ZIP -> "zip" + } + ) } result } diff --git a/gbcs-memcached/src/main/resources/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd b/gbcs-memcached/src/main/resources/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd index a219a9a..32af191 100644 --- a/gbcs-memcached/src/main/resources/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd +++ b/gbcs-memcached/src/main/resources/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd @@ -20,14 +20,14 @@ - + - + diff --git a/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt b/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt index 1911fe6..bd4b92d 100644 --- a/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt +++ b/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt @@ -102,6 +102,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) { private class ClientCertificateAuthenticator( authorizer: Authorizer, private val sslEngine: SSLEngine, + private val anonymousUserRoles: Set?, private val userExtractor: Configuration.UserExtractor?, private val groupExtractor: Configuration.GroupExtractor?, ) : AbstractNettyHttpAuthenticator(authorizer) { @@ -112,16 +113,16 @@ class GradleBuildCacheServer(private val cfg: Configuration) { override fun authenticate(ctx: ChannelHandlerContext, req: HttpRequest): Set? { return try { - sslEngine.session.peerCertificates + sslEngine.session.peerCertificates.takeIf { + it.isNotEmpty() + }?.let { peerCertificates -> + val clientCertificate = peerCertificates.first() as X509Certificate + val user = userExtractor?.extract(clientCertificate) + val group = groupExtractor?.extract(clientCertificate) + (group?.roles ?: emptySet()) + (user?.roles ?: emptySet()) + } ?: anonymousUserRoles } catch (es: SSLPeerUnverifiedException) { - null - }?.takeIf { - it.isNotEmpty() - }?.let { peerCertificates -> - val clientCertificate = peerCertificates.first() as X509Certificate - val user = userExtractor?.extract(clientCertificate) - val group = groupExtractor?.extract(clientCertificate) - (group?.roles ?: emptySet()) + (user?.roles ?: emptySet()) + anonymousUserRoles } } } @@ -139,21 +140,21 @@ class GradleBuildCacheServer(private val cfg: Configuration) { log.debug(ctx) { "Missing Authorization header" } - return null + return users[""]?.roles } val cursor = authorizationHeader.indexOf(' ') if (cursor < 0) { log.debug(ctx) { "Invalid Authorization header: '$authorizationHeader'" } - return null + return users[""]?.roles } val authenticationType = authorizationHeader.substring(0, cursor) if ("Basic" != authenticationType) { log.debug(ctx) { "Invalid authentication type header: '$authenticationType'" } - return null + return users[""]?.roles } val (username, password) = Base64.getDecoder().decode(authorizationHeader.substring(cursor + 1)) .let(::String) @@ -199,8 +200,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) { SslContextBuilder.forServer(serverKey, *serverCert).apply { if (tls.isVerifyClients) { clientAuth(ClientAuth.OPTIONAL) - val trustStore = tls.trustStore - if (trustStore != null) { + tls.trustStore?.let { trustStore -> val ts = loadKeystore(trustStore.file, trustStore.password) trustManager( ClientCertificateValidator.getTrustManager(ts, trustStore.isCheckCertificateStatus) @@ -268,18 +268,17 @@ class GradleBuildCacheServer(private val cfg: Configuration) { val auth = cfg.authentication var authenticator: AbstractNettyHttpAuthenticator? = null if (auth is Configuration.BasicAuthentication) { - val roleAuthorizer = RoleAuthorizer() - authenticator = (NettyHttpBasicAuthenticator(cfg.users, roleAuthorizer)) + authenticator = (NettyHttpBasicAuthenticator(cfg.users, RoleAuthorizer())) } if (sslContext != null) { val sslHandler = sslContext.newHandler(ch.alloc()) pipeline.addLast(sslHandler) if (auth is Configuration.ClientCertificateAuthentication) { - val roleAuthorizer = RoleAuthorizer() authenticator = ClientCertificateAuthenticator( - roleAuthorizer, + RoleAuthorizer(), sslHandler.engine(), + cfg.users[""]?.roles, userExtractor(auth), groupExtractor(auth) ) @@ -446,7 +445,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) { class ServerHandle( httpChannelFuture: ChannelFuture, - private val executorGroups : Iterable + private val executorGroups: Iterable ) : AutoCloseable { private val httpChannel: Channel = httpChannelFuture.channel() @@ -476,7 +475,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) { val serverSocketChannel = NioServerSocketChannel::class.java val workerGroup = bossGroup val eventExecutorGroup = run { - val threadFactory = if(cfg.isUseVirtualThread) { + val threadFactory = if (cfg.isUseVirtualThread) { Thread.ofVirtual().factory() } else { null diff --git a/src/main/kotlin/net/woggioni/gbcs/auth/ClientCertificateValidator.kt b/src/main/kotlin/net/woggioni/gbcs/auth/ClientCertificateValidator.kt index 8af44a7..28e8821 100644 --- a/src/main/kotlin/net/woggioni/gbcs/auth/ClientCertificateValidator.kt +++ b/src/main/kotlin/net/woggioni/gbcs/auth/ClientCertificateValidator.kt @@ -1,16 +1,18 @@ package net.woggioni.gbcs.auth +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInboundHandlerAdapter +import io.netty.handler.ssl.SslHandler +import io.netty.handler.ssl.SslHandshakeCompletionEvent import java.security.KeyStore import java.security.cert.CertPathValidator +import java.security.cert.CertPathValidatorException +import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.PKIXParameters import java.security.cert.PKIXRevocationChecker import java.security.cert.X509Certificate import java.util.EnumSet -import io.netty.channel.ChannelHandlerContext -import io.netty.channel.ChannelInboundHandlerAdapter -import io.netty.handler.ssl.SslHandler -import io.netty.handler.ssl.SslHandshakeCompletionEvent import javax.net.ssl.SSLSession import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager @@ -48,7 +50,11 @@ class ClientCertificateValidator private constructor( object : X509TrustManager { override fun checkClientTrusted(chain: Array, authType: String) { val clientCertificateChain = certificateFactory.generateCertPath(chain.toList()) - validator.validate(clientCertificateChain, params) + try { + validator.validate(clientCertificateChain, params) + } catch (ex : CertPathValidatorException) { + throw CertificateException(ex) + } } override fun checkServerTrusted(chain: Array, authType: String) { diff --git a/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt b/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt index f6ed628..902939c 100644 --- a/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt +++ b/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt @@ -23,10 +23,11 @@ object Parser { fun parse(document: Document): Configuration { val root = document.documentElement + val anonymousUser = User("", null, emptySet()) var cache: Cache? = null var host = "127.0.0.1" var port = 11080 - var users = emptyMap() + var users : Map = mapOf(anonymousUser.name to anonymousUser) var groups = emptyMap() var tls: Tls? = null val serverPath = root.getAttribute("path") @@ -35,16 +36,17 @@ object Parser { ?.let(String::toBoolean) ?: true var authentication: Authentication? = null for (child in root.asIterable()) { - when (child.localName) { + val tagName = child.localName + when (tagName) { "authorization" -> { + var knownUsers = sequenceOf(anonymousUser) for (gchild in child.asIterable()) { - when (child.localName) { + when (gchild.localName) { "users" -> { - users = parseUsers(child) + knownUsers += parseUsers(gchild) } - "groups" -> { - val pair = parseGroups(child, users) + val pair = parseGroups(gchild, knownUsers) users = pair.first groups = pair.second } @@ -76,17 +78,17 @@ object Parser { "client-certificate" -> { var tlsExtractorUser: TlsCertificateExtractor? = null var tlsExtractorGroup: TlsCertificateExtractor? = null - for (gchild in child.asIterable()) { - when (gchild.localName) { + for (ggchild in gchild.asIterable()) { + when (ggchild.localName) { "group-extractor" -> { - val attrName = gchild.getAttribute("attribute-name") - val pattern = gchild.getAttribute("pattern") + val attrName = ggchild.getAttribute("attribute-name") + val pattern = ggchild.getAttribute("pattern") tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern) } "user-extractor" -> { - val attrName = gchild.getAttribute("attribute-name") - val pattern = gchild.getAttribute("pattern") + val attrName = ggchild.getAttribute("attribute-name") + val pattern = ggchild.getAttribute("pattern") tlsExtractorUser = TlsCertificateExtractor(attrName, pattern) } } @@ -151,23 +153,22 @@ object Parser { } }.toSet() - private fun parseUserRefs(root: Element) = root.asIterable().asSequence().filter { - it.localName == "user" - }.map { + private fun parseUserRefs(root: Element) = root.asIterable().asSequence().map { it.getAttribute("ref") }.toSet() - private fun parseUsers(root: Element): Map { + private fun parseUsers(root: Element): Sequence { return root.asIterable().asSequence().filter { it.localName == "user" }.map { el -> val username = el.getAttribute("name") val password = el.getAttribute("password").takeIf(String::isNotEmpty) - username to User(username, password, emptySet()) - }.toMap() + User(username, password, emptySet()) + } } - private fun parseGroups(root: Element, knownUsers: Map): Pair, Map> { + private fun parseGroups(root: Element, knownUsers: Sequence): Pair, Map> { + val knownUsersMap = knownUsers.associateBy(User::getName) val userGroups = mutableMapOf>() val groups = root.asIterable().asSequence().filter { it.localName == "group" @@ -177,7 +178,7 @@ object Parser { for (child in el.asIterable()) { when (child.localName) { "users" -> { - parseUserRefs(child).mapNotNull(knownUsers::get).forEach { user -> + parseUserRefs(child).mapNotNull(knownUsersMap::get).forEach { user -> userGroups.computeIfAbsent(user.name) { mutableSetOf() }.add(groupName) @@ -191,7 +192,7 @@ object Parser { } groupName to Group(groupName, roles) }.toMap() - val users = knownUsers.map { (name, user) -> + val users = knownUsersMap.map { (name, user) -> name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet()) }.toMap() return users to groups diff --git a/src/main/kotlin/net/woggioni/gbcs/configuration/Serializer.kt b/src/main/kotlin/net/woggioni/gbcs/configuration/Serializer.kt index 07bde9d..28c46a3 100644 --- a/src/main/kotlin/net/woggioni/gbcs/configuration/Serializer.kt +++ b/src/main/kotlin/net/woggioni/gbcs/configuration/Serializer.kt @@ -17,7 +17,7 @@ object Serializer { attr("useVirtualThreads", conf.isUseVirtualThread.toString()) // attr("xmlns:xs", GradleBuildCacheServer.XML_SCHEMA_NAMESPACE_URI) val value = schemaLocations.asSequence().map { (k, v) -> "$k $v" }.joinToString(" ") - attr("xs:schemaLocation",value , namespaceURI = GBCS.XML_SCHEMA_NAMESPACE_URI) + attr("xs:schemaLocation", value , namespaceURI = GBCS.XML_SCHEMA_NAMESPACE_URI) conf.serverPath ?.takeIf(String::isNotEmpty) @@ -35,10 +35,12 @@ object Serializer { node("authorization") { node("users") { for(user in conf.users.values) { - node("user") { - attr("name", user.name) - user.password?.let { password -> - attr("password", password) + if(user.name.isNotEmpty()) { + node("user") { + attr("name", user.name) + user.password?.let { password -> + attr("password", password) + } } } } @@ -55,11 +57,19 @@ object Serializer { attr("name", group.name) if(users.isNotEmpty()) { node("users") { + var anonymousUser : Configuration.User? = null for(user in users) { - node("user") { - attr("ref", user.name) + if(user.name.isNotEmpty()) { + node("user") { + attr("ref", user.name) + } + } else { + anonymousUser = user } } + if(anonymousUser != null) { + node("anonymous") + } } } if(group.roles.isNotEmpty()) { @@ -82,6 +92,12 @@ object Serializer { } is Configuration.ClientCertificateAuthentication -> { node("client-certificate") { + authentication.groupExtractor?.let { extractor -> + node("group-extractor") { + attr("attribute-name", extractor.rdnType) + attr("pattern", extractor.pattern) + } + } authentication.userExtractor?.let { extractor -> node("user-extractor") { attr("attribute-name", extractor.rdnType) diff --git a/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd b/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd index f302ac4..cb62b35 100644 --- a/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd +++ b/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd @@ -21,7 +21,6 @@ - @@ -127,7 +126,8 @@ - + + diff --git a/src/test/kotlin/net/woggioni/gbcs/test/AbstractBasicAuthServerTest.kt b/src/test/kotlin/net/woggioni/gbcs/test/AbstractBasicAuthServerTest.kt new file mode 100644 index 0000000..16f9709 --- /dev/null +++ b/src/test/kotlin/net/woggioni/gbcs/test/AbstractBasicAuthServerTest.kt @@ -0,0 +1,76 @@ +package net.woggioni.gbcs.test + +import net.woggioni.gbcs.api.Configuration +import net.woggioni.gbcs.api.Role +import net.woggioni.gbcs.base.Xml +import net.woggioni.gbcs.cache.FileSystemCacheConfiguration +import net.woggioni.gbcs.configuration.Serializer +import net.woggioni.gbcs.utils.NetworkUtils +import java.net.URI +import java.net.http.HttpRequest +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import java.time.Duration +import java.util.Base64 +import java.util.zip.Deflater +import kotlin.random.Random + + +abstract class AbstractBasicAuthServerTest : AbstractServerTest() { + + private lateinit var cacheDir : Path + + protected val random = Random(101325) + protected val keyValuePair = newEntry(random) + protected val serverPath = "gbcs" + protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader)) + protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer)) + + abstract protected val users : List + + override fun setUp() { + this.cacheDir = testDir.resolve("cache") + cfg = Configuration( + "127.0.0.1", + NetworkUtils.getFreePort(), + serverPath, + users.asSequence().map { it.name to it}.toMap(), + sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(), + FileSystemCacheConfiguration(this.cacheDir, + maxAge = Duration.ofSeconds(3600 * 24), + digestAlgorithm = "MD5", + compressionLevel = Deflater.DEFAULT_COMPRESSION, + compressionEnabled = false + ), + Configuration.BasicAuthentication(), + null, + true, + ) + Xml.write(Serializer.serialize(cfg), System.out) + } + + override fun tearDown() { + } + + protected fun buildAuthorizationHeader(user : Configuration.User, password : String) : String { + val b64 = Base64.getEncoder().encode("${user.name}:${password}".toByteArray(Charsets.UTF_8)).let{ + String(it, StandardCharsets.UTF_8) + } + return "Basic $b64" + } + + protected fun newRequestBuilder(key : String) = HttpRequest.newBuilder() + .uri(URI.create("http://${cfg.host}:${cfg.port}/$serverPath/$key")) + + + protected fun newEntry(random : Random) : Pair { + val key = ByteArray(0x10).let { + random.nextBytes(it) + Base64.getUrlEncoder().encodeToString(it) + } + val value = ByteArray(0x1000).also { + random.nextBytes(it) + } + return key to value + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/woggioni/gbcs/test/AbstractTlsServerTest.kt b/src/test/kotlin/net/woggioni/gbcs/test/AbstractTlsServerTest.kt new file mode 100644 index 0000000..5b162dc --- /dev/null +++ b/src/test/kotlin/net/woggioni/gbcs/test/AbstractTlsServerTest.kt @@ -0,0 +1,192 @@ +package net.woggioni.gbcs.test + +import io.netty.handler.codec.http.HttpResponseStatus +import net.woggioni.gbcs.api.Configuration +import net.woggioni.gbcs.api.Role +import net.woggioni.gbcs.base.Xml +import net.woggioni.gbcs.cache.FileSystemCacheConfiguration +import net.woggioni.gbcs.configuration.Serializer +import net.woggioni.gbcs.utils.CertificateUtils +import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials +import net.woggioni.gbcs.utils.NetworkUtils +import org.bouncycastle.asn1.x500.X500Name +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.security.KeyStore +import java.security.KeyStore.PasswordProtection +import java.time.Duration +import java.util.Base64 +import java.util.zip.Deflater +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import kotlin.random.Random + + +abstract class AbstractTlsServerTest : AbstractServerTest() { + + companion object { + private const val CA_CERTIFICATE_ENTRY = "gbcs-ca" + private const val CLIENT_CERTIFICATE_ENTRY = "gbcs-client" + private const val SERVER_CERTIFICATE_ENTRY = "gbcs-server" + private const val PASSWORD = "password" + } + + private lateinit var cacheDir: Path + private lateinit var serverKeyStoreFile: Path + private lateinit var clientKeyStoreFile: Path + private lateinit var trustStoreFile: Path + private lateinit var serverKeyStore: KeyStore + private lateinit var clientKeyStore: KeyStore + private lateinit var trustStore: KeyStore + protected lateinit var ca: X509Credentials + + protected val readersGroup = Configuration.Group("readers", setOf(Role.Reader)) + protected val writersGroup = Configuration.Group("writers", setOf(Role.Writer)) + protected val random = Random(101325) + protected val keyValuePair = newEntry(random) + private val serverPath : String? = null + + protected abstract val users : List + + protected fun createKeyStoreAndTrustStore() { + ca = CertificateUtils.createCertificateAuthority(CA_CERTIFICATE_ENTRY, 30) + val serverCert = CertificateUtils.createServerCertificate(ca, X500Name("CN=$SERVER_CERTIFICATE_ENTRY"), 30) + val clientCert = CertificateUtils.createClientCertificate(ca, X500Name("CN=$CLIENT_CERTIFICATE_ENTRY"), 30) + + serverKeyStore = KeyStore.getInstance("PKCS12").apply { + load(null, null) + setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null)) + setEntry( + SERVER_CERTIFICATE_ENTRY, + KeyStore.PrivateKeyEntry( + serverCert.keyPair().private, + arrayOf(serverCert.certificate(), ca.certificate) + ), + PasswordProtection(PASSWORD.toCharArray()) + ) + } + Files.newOutputStream(this.serverKeyStoreFile).use { + serverKeyStore.store(it, null) + } + + clientKeyStore = KeyStore.getInstance("PKCS12").apply { + load(null, null) + setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null)) + setEntry( + CLIENT_CERTIFICATE_ENTRY, + KeyStore.PrivateKeyEntry( + clientCert.keyPair().private, + arrayOf(clientCert.certificate(), ca.certificate) + ), + PasswordProtection(PASSWORD.toCharArray()) + ) + } + Files.newOutputStream(this.clientKeyStoreFile).use { + clientKeyStore.store(it, null) + } + + trustStore = KeyStore.getInstance("PKCS12").apply { + load(null, null) + setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null)) + } + Files.newOutputStream(this.trustStoreFile).use { + trustStore.store(it, null) + } + } + + protected fun getClientKeyStore(ca: X509Credentials, subject: X500Name) = KeyStore.getInstance("PKCS12").apply { + val clientCert = CertificateUtils.createClientCertificate(ca, subject, 30) + + load(null, null) + setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null)) + setEntry( + CLIENT_CERTIFICATE_ENTRY, + KeyStore.PrivateKeyEntry(clientCert.keyPair().private, arrayOf(clientCert.certificate(), ca.certificate)), + PasswordProtection(PASSWORD.toCharArray()) + ) + } + + protected fun getHttpClient(clientKeyStore: KeyStore?): HttpClient { + val kmf = clientKeyStore?.let { + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { + init(it, PASSWORD.toCharArray()) + } + } + + + // Set up trust manager factory with the truststore + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + tmf.init(trustStore) + + // Create SSL context with the key and trust managers + val sslContext = SSLContext.getInstance("TLS").apply { + init(kmf?.keyManagers ?: emptyArray(), tmf.trustManagers, null) + } + return HttpClient.newBuilder().sslContext(sslContext).build() + } + + override fun setUp() { + this.clientKeyStoreFile = testDir.resolve("client-keystore.p12") + this.serverKeyStoreFile = testDir.resolve("server-keystore.p12") + this.trustStoreFile = testDir.resolve("truststore.p12") + this.cacheDir = testDir.resolve("cache") + createKeyStoreAndTrustStore() + cfg = Configuration( + "127.0.0.1", + NetworkUtils.getFreePort(), + serverPath, + users.asSequence().map { it.name to it }.toMap(), + sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(), + FileSystemCacheConfiguration(this.cacheDir, + maxAge = Duration.ofSeconds(3600 * 24), + compressionEnabled = true, + compressionLevel = Deflater.DEFAULT_COMPRESSION, + digestAlgorithm = "MD5" + ), + Configuration.ClientCertificateAuthentication( + Configuration.TlsCertificateExtractor("CN", "(.*)"), + null + ), + Configuration.Tls( + Configuration.KeyStore(this.serverKeyStoreFile, null, SERVER_CERTIFICATE_ENTRY, PASSWORD), + Configuration.TrustStore(this.trustStoreFile, null, false), + true + ), + false, + ) + Xml.write(Serializer.serialize(cfg), System.out) + } + + override fun tearDown() { + } + + protected fun newRequestBuilder(key: String) = HttpRequest.newBuilder() + .uri(URI.create("https://${cfg.host}:${cfg.port}/${serverPath ?: ""}/$key")) + + private fun buildAuthorizationHeader(user: Configuration.User, password: String): String { + val b64 = Base64.getEncoder().encode("${user.name}:${password}".toByteArray(Charsets.UTF_8)).let { + String(it, StandardCharsets.UTF_8) + } + return "Basic $b64" + } + + protected fun newEntry(random: Random): Pair { + val key = ByteArray(0x10).let { + random.nextBytes(it) + Base64.getUrlEncoder().encodeToString(it) + } + val value = ByteArray(0x1000).also { + random.nextBytes(it) + } + return key to value + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt b/src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt index ae559ee..1fcb15e 100644 --- a/src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt +++ b/src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt @@ -4,90 +4,26 @@ import io.netty.handler.codec.http.HttpResponseStatus import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.base.PasswordSecurity.hashPassword -import net.woggioni.gbcs.base.Xml -import net.woggioni.gbcs.cache.FileSystemCacheConfiguration -import net.woggioni.gbcs.configuration.Serializer -import net.woggioni.gbcs.utils.NetworkUtils import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test -import java.io.IOException -import java.net.ServerSocket -import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse -import java.nio.charset.StandardCharsets -import java.nio.file.Path -import java.time.Duration -import java.util.Base64 -import java.util.zip.Deflater -import kotlin.random.Random -class BasicAuthServerTest : AbstractServerTest() { +class BasicAuthServerTest : AbstractBasicAuthServerTest() { companion object { private const val PASSWORD = "password" } - private lateinit var cacheDir : Path - - private val random = Random(101325) - private val keyValuePair = newEntry(random) - private val serverPath = "gbcs" - - override fun setUp() { - this.cacheDir = testDir.resolve("cache") - val readersGroup = Configuration.Group("readers", setOf(Role.Reader)) - val writersGroup = Configuration.Group("writers", setOf(Role.Writer)) - cfg = Configuration( - "127.0.0.1", - NetworkUtils.getFreePort(), - serverPath, - listOf( - Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)), - Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup)), - Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup)) - ).asSequence().map { it.name to it}.toMap(), - sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(), - FileSystemCacheConfiguration(this.cacheDir, - maxAge = Duration.ofSeconds(3600 * 24), - digestAlgorithm = "MD5", - compressionLevel = Deflater.DEFAULT_COMPRESSION, - compressionEnabled = false - ), - Configuration.BasicAuthentication(), - null, - true, - ) - Xml.write(Serializer.serialize(cfg), System.out) - } - - override fun tearDown() { - } - - fun buildAuthorizationHeader(user : Configuration.User, password : String) : String { - val b64 = Base64.getEncoder().encode("${user.name}:${password}".toByteArray(Charsets.UTF_8)).let{ - String(it, StandardCharsets.UTF_8) - } - return "Basic $b64" - } - - fun newRequestBuilder(key : String) = HttpRequest.newBuilder() - .uri(URI.create("http://${cfg.host}:${cfg.port}/$serverPath/$key")) - - - fun newEntry(random : Random) : Pair { - val key = ByteArray(0x10).let { - random.nextBytes(it) - Base64.getUrlEncoder().encodeToString(it) - } - val value = ByteArray(0x1000).also { - random.nextBytes(it) - } - return key to value - } + override val users = listOf( + Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)), + Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup)), + Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup)), + Configuration.User("", null, setOf(readersGroup)) + ) @Test @Order(1) @@ -100,7 +36,7 @@ class BasicAuthServerTest : AbstractServerTest() { .PUT(HttpRequest.BodyPublishers.ofByteArray(value)) val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) - Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode()) + Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode()) } @Test @@ -179,6 +115,20 @@ class BasicAuthServerTest : AbstractServerTest() { @Test @Order(6) + fun getAsAnonymousUser() { + val client: HttpClient = HttpClient.newHttpClient() + val (key, value) = keyValuePair + + val requestBuilder = newRequestBuilder(key) + .GET() + + val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()) + Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode()) + Assertions.assertArrayEquals(value, response.body()) + } + + @Test + @Order(7) fun getMissingKeyAsAReaderUser() { val client: HttpClient = HttpClient.newHttpClient() diff --git a/src/test/kotlin/net/woggioni/gbcs/test/ConfigurationTest.kt b/src/test/kotlin/net/woggioni/gbcs/test/ConfigurationTest.kt index 0e41bb2..491b5d9 100644 --- a/src/test/kotlin/net/woggioni/gbcs/test/ConfigurationTest.kt +++ b/src/test/kotlin/net/woggioni/gbcs/test/ConfigurationTest.kt @@ -1,7 +1,7 @@ package net.woggioni.gbcs.test -import net.woggioni.gbcs.base.GbcsUrlStreamHandlerFactory import net.woggioni.gbcs.base.GBCS.toUrl +import net.woggioni.gbcs.base.GbcsUrlStreamHandlerFactory import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.configuration.Parser import net.woggioni.gbcs.configuration.Serializer @@ -18,6 +18,7 @@ class ConfigurationTest { strings = [ "classpath:net/woggioni/gbcs/test/gbcs-default.xml", "classpath:net/woggioni/gbcs/test/gbcs-memcached.xml", + "classpath:net/woggioni/gbcs/test/gbcs-tls.xml", ] ) @ParameterizedTest diff --git a/src/test/kotlin/net/woggioni/gbcs/test/NoAnonymousUserBasicAuthServerTest.kt b/src/test/kotlin/net/woggioni/gbcs/test/NoAnonymousUserBasicAuthServerTest.kt new file mode 100644 index 0000000..f0c6bd3 --- /dev/null +++ b/src/test/kotlin/net/woggioni/gbcs/test/NoAnonymousUserBasicAuthServerTest.kt @@ -0,0 +1,53 @@ +package net.woggioni.gbcs.test + +import io.netty.handler.codec.http.HttpResponseStatus +import net.woggioni.gbcs.api.Configuration +import net.woggioni.gbcs.api.Role +import net.woggioni.gbcs.base.PasswordSecurity.hashPassword +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + + +class NoAnonymousUserBasicAuthServerTest : AbstractBasicAuthServerTest() { + + companion object { + private const val PASSWORD = "anotherPassword" + } + + override val users = listOf( + Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)), + Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup)), + Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup)), + ) + + @Test + @Order(1) + fun putWithNoAuthorizationHeader() { + val client: HttpClient = HttpClient.newHttpClient() + val (key, value) = keyValuePair + + val requestBuilder = newRequestBuilder(key) + .header("Content-Type", "application/octet-stream") + .PUT(HttpRequest.BodyPublishers.ofByteArray(value)) + + val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) + Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode()) + } + + @Test + @Order(2) + fun getWithNoAuthorizationHeader() { + val client: HttpClient = HttpClient.newHttpClient() + val (key, value) = keyValuePair + + val requestBuilder = newRequestBuilder(key) + .GET() + + val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()) + Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/woggioni/gbcs/test/NoAnonymousUserTlsServerTest.kt b/src/test/kotlin/net/woggioni/gbcs/test/NoAnonymousUserTlsServerTest.kt new file mode 100644 index 0000000..38ab836 --- /dev/null +++ b/src/test/kotlin/net/woggioni/gbcs/test/NoAnonymousUserTlsServerTest.kt @@ -0,0 +1,47 @@ +package net.woggioni.gbcs.test + +import io.netty.handler.codec.http.HttpResponseStatus +import net.woggioni.gbcs.api.Configuration +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +class NoAnonymousUserTlsServerTest : AbstractTlsServerTest() { + + override val users = listOf( + Configuration.User("user1", null, setOf(readersGroup)), + Configuration.User("user2", null, setOf(writersGroup)), + Configuration.User("user3", null, setOf(readersGroup, writersGroup)), + ) + + @Test + @Order(1) + fun getAsAnonymousUser() { + val (key, _) = keyValuePair + val client: HttpClient = getHttpClient(null) + + val requestBuilder = newRequestBuilder(key) + .header("Content-Type", "application/octet-stream") + .GET() + + val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()) + Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode()) + } + + @Test + @Order(2) + fun putAsAnonymousUser() { + val (key, value) = keyValuePair + val client: HttpClient = getHttpClient(null) + + val requestBuilder = newRequestBuilder(key) + .header("Content-Type", "application/octet-stream") + .PUT(HttpRequest.BodyPublishers.ofByteArray(value)) + + val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) + Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/woggioni/gbcs/test/TlsServerTest.kt b/src/test/kotlin/net/woggioni/gbcs/test/TlsServerTest.kt index 90fcf4a..2c5afb4 100644 --- a/src/test/kotlin/net/woggioni/gbcs/test/TlsServerTest.kt +++ b/src/test/kotlin/net/woggioni/gbcs/test/TlsServerTest.kt @@ -32,185 +32,17 @@ import javax.net.ssl.TrustManagerFactory import kotlin.random.Random -class TlsServerTest : AbstractServerTest() { +class TlsServerTest : AbstractTlsServerTest() { - companion object { - private const val CA_CERTIFICATE_ENTRY = "gbcs-ca" - private const val CLIENT_CERTIFICATE_ENTRY = "gbcs-client" - private const val SERVER_CERTIFICATE_ENTRY = "gbcs-server" - private const val PASSWORD = "password" - } - - private lateinit var cacheDir: Path - private lateinit var serverKeyStoreFile: Path - private lateinit var clientKeyStoreFile: Path - private lateinit var trustStoreFile: Path - private lateinit var serverKeyStore: KeyStore - private lateinit var clientKeyStore: KeyStore - private lateinit var trustStore: KeyStore - private lateinit var ca: X509Credentials - - private val readersGroup = Configuration.Group("readers", setOf(Role.Reader)) - private val writersGroup = Configuration.Group("writers", setOf(Role.Writer)) - private val random = Random(101325) - private val keyValuePair = newEntry(random) - private val serverPath : String? = null - - private val users = listOf( + override val users = listOf( Configuration.User("user1", null, setOf(readersGroup)), Configuration.User("user2", null, setOf(writersGroup)), - Configuration.User("user3", null, setOf(readersGroup, writersGroup)) + Configuration.User("user3", null, setOf(readersGroup, writersGroup)), + Configuration.User("", null, setOf(readersGroup)) ) - fun createKeyStoreAndTrustStore() { - ca = CertificateUtils.createCertificateAuthority(CA_CERTIFICATE_ENTRY, 30) - val serverCert = CertificateUtils.createServerCertificate(ca, X500Name("CN=$SERVER_CERTIFICATE_ENTRY"), 30) - val clientCert = CertificateUtils.createClientCertificate(ca, X500Name("CN=$CLIENT_CERTIFICATE_ENTRY"), 30) - - serverKeyStore = KeyStore.getInstance("PKCS12").apply { - load(null, null) - setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null)) - setEntry( - SERVER_CERTIFICATE_ENTRY, - KeyStore.PrivateKeyEntry( - serverCert.keyPair().private, - arrayOf(serverCert.certificate(), ca.certificate) - ), - PasswordProtection(PASSWORD.toCharArray()) - ) - } - Files.newOutputStream(this.serverKeyStoreFile).use { - serverKeyStore.store(it, null) - } - - clientKeyStore = KeyStore.getInstance("PKCS12").apply { - load(null, null) - setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null)) - setEntry( - CLIENT_CERTIFICATE_ENTRY, - KeyStore.PrivateKeyEntry( - clientCert.keyPair().private, - arrayOf(clientCert.certificate(), ca.certificate) - ), - PasswordProtection(PASSWORD.toCharArray()) - ) - } - Files.newOutputStream(this.clientKeyStoreFile).use { - clientKeyStore.store(it, null) - } - - trustStore = KeyStore.getInstance("PKCS12").apply { - load(null, null) - setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null)) - } - Files.newOutputStream(this.trustStoreFile).use { - trustStore.store(it, null) - } - } - - fun getClientKeyStore(ca: X509Credentials, subject: X500Name) = KeyStore.getInstance("PKCS12").apply { - val clientCert = CertificateUtils.createClientCertificate(ca, subject, 30) - - load(null, null) - setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null)) - setEntry( - CLIENT_CERTIFICATE_ENTRY, - KeyStore.PrivateKeyEntry(clientCert.keyPair().private, arrayOf(clientCert.certificate(), ca.certificate)), - PasswordProtection(PASSWORD.toCharArray()) - ) - } - - fun getHttpClient(clientKeyStore: KeyStore?): HttpClient { - val kmf = clientKeyStore?.let { - KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { - init(it, PASSWORD.toCharArray()) - } - } - - - // Set up trust manager factory with the truststore - val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - tmf.init(trustStore) - - // Create SSL context with the key and trust managers - val sslContext = SSLContext.getInstance("TLS").apply { - init(kmf?.keyManagers ?: emptyArray(), tmf.trustManagers, null) - } - return HttpClient.newBuilder().sslContext(sslContext).build() - } - - override fun setUp() { - this.clientKeyStoreFile = testDir.resolve("client-keystore.p12") - this.serverKeyStoreFile = testDir.resolve("server-keystore.p12") - this.trustStoreFile = testDir.resolve("truststore.p12") - this.cacheDir = testDir.resolve("cache") - createKeyStoreAndTrustStore() - cfg = Configuration( - "127.0.0.1", - NetworkUtils.getFreePort(), - serverPath, - users.asSequence().map { it.name to it }.toMap(), - sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(), - FileSystemCacheConfiguration(this.cacheDir, - maxAge = Duration.ofSeconds(3600 * 24), - compressionEnabled = true, - compressionLevel = Deflater.DEFAULT_COMPRESSION, - digestAlgorithm = "MD5" - ), - Configuration.ClientCertificateAuthentication( - Configuration.TlsCertificateExtractor("CN", "(.*)"), - null - ), - Configuration.Tls( - Configuration.KeyStore(this.serverKeyStoreFile, null, SERVER_CERTIFICATE_ENTRY, PASSWORD), - Configuration.TrustStore(this.trustStoreFile, null, false), - true - ), - false, - ) - Xml.write(Serializer.serialize(cfg), System.out) - } - - override fun tearDown() { - } - - fun newRequestBuilder(key: String) = HttpRequest.newBuilder() - .uri(URI.create("https://${cfg.host}:${cfg.port}/${serverPath ?: ""}/$key")) - - fun buildAuthorizationHeader(user: Configuration.User, password: String): String { - val b64 = Base64.getEncoder().encode("${user.name}:${password}".toByteArray(Charsets.UTF_8)).let { - String(it, StandardCharsets.UTF_8) - } - return "Basic $b64" - } - - fun newEntry(random: Random): Pair { - val key = ByteArray(0x10).let { - random.nextBytes(it) - Base64.getUrlEncoder().encodeToString(it) - } - val value = ByteArray(0x1000).also { - random.nextBytes(it) - } - return key to value - } - @Test @Order(1) - fun putWithNoClientCertificate() { - val client: HttpClient = getHttpClient(null) - val (key, value) = keyValuePair - - val requestBuilder = newRequestBuilder(key) - .header("Content-Type", "application/octet-stream") - .PUT(HttpRequest.BodyPublishers.ofByteArray(value)) - - val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) - Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode()) - } - - @Test - @Order(2) fun putAsAReaderUser() { val (key, value) = keyValuePair val user = cfg.users.values.find { @@ -226,7 +58,7 @@ class TlsServerTest : AbstractServerTest() { } @Test - @Order(3) + @Order(2) fun getAsAWriterUser() { val (key, _) = keyValuePair val user = cfg.users.values.find { @@ -235,7 +67,6 @@ class TlsServerTest : AbstractServerTest() { val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}"))) val requestBuilder = newRequestBuilder(key) - .header("Authorization", buildAuthorizationHeader(user, PASSWORD)) .GET() val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) @@ -243,7 +74,7 @@ class TlsServerTest : AbstractServerTest() { } @Test - @Order(4) + @Order(3) fun putAsAWriterUser() { val (key, value) = keyValuePair val user = cfg.users.values.find { @@ -253,7 +84,6 @@ class TlsServerTest : AbstractServerTest() { val requestBuilder = newRequestBuilder(key) .header("Content-Type", "application/octet-stream") - .header("Authorization", buildAuthorizationHeader(user, PASSWORD)) .PUT(HttpRequest.BodyPublishers.ofByteArray(value)) val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) @@ -261,7 +91,7 @@ class TlsServerTest : AbstractServerTest() { } @Test - @Order(5) + @Order(4) fun getAsAReaderUser() { val (key, value) = keyValuePair val user = cfg.users.values.find { @@ -270,7 +100,6 @@ class TlsServerTest : AbstractServerTest() { val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}"))) val requestBuilder = newRequestBuilder(key) - .header("Authorization", buildAuthorizationHeader(user, PASSWORD)) .GET() val response: HttpResponse = @@ -280,7 +109,7 @@ class TlsServerTest : AbstractServerTest() { } @Test - @Order(6) + @Order(5) fun getMissingKeyAsAReaderUser() { val (key, _) = newEntry(random) val user = cfg.users.values.find { @@ -289,11 +118,39 @@ class TlsServerTest : AbstractServerTest() { val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}"))) val requestBuilder = newRequestBuilder(key) - .header("Authorization", buildAuthorizationHeader(user, PASSWORD)) .GET() val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()) Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode()) } + + @Test + @Order(6) + fun getAsAnonymousUser() { + val (key, value) = keyValuePair + val client: HttpClient = getHttpClient(null) + + val requestBuilder = newRequestBuilder(key) + .header("Content-Type", "application/octet-stream") + .GET() + + val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray()) + Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode()) + Assertions.assertArrayEquals(value, response.body()) + } + + @Test + @Order(7) + fun putAsAnonymousUser() { + val (key, value) = keyValuePair + val client: HttpClient = getHttpClient(null) + + val requestBuilder = newRequestBuilder(key) + .header("Content-Type", "application/octet-stream") + .PUT(HttpRequest.BodyPublishers.ofByteArray(value)) + + val response: HttpResponse = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) + Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode()) + } } \ No newline at end of file diff --git a/src/test/resources/net/woggioni/gbcs/test/gbcs-memcached.xml b/src/test/resources/net/woggioni/gbcs/test/gbcs-memcached.xml index 918a44d..bc04186 100644 --- a/src/test/resources/net/woggioni/gbcs/test/gbcs-memcached.xml +++ b/src/test/resources/net/woggioni/gbcs/test/gbcs-memcached.xml @@ -4,7 +4,7 @@ xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached" xs:schemaLocation="urn:net.woggioni.gbcs-memcached jpms://net.woggioni.gbcs.memcached/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd urn:net.woggioni.gbcs jpms://net.woggioni.gbcs/net/woggioni/gbcs/schema/gbcs.xsd"> - + diff --git a/src/test/resources/net/woggioni/gbcs/test/gbcs-tls.xml b/src/test/resources/net/woggioni/gbcs/test/gbcs-tls.xml new file mode 100644 index 0000000..32a7eb1 --- /dev/null +++ b/src/test/resources/net/woggioni/gbcs/test/gbcs-tls.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file