Harden admin deployment and local acceptance
Enforce mTLS and least-privilege runtime boundaries while adding repeatable MySQL 8.4 and Docker smoke checks that require no production secrets.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package com.osglab.account.config
|
||||
|
||||
import ch.qos.logback.classic.LoggerContext
|
||||
import ch.qos.logback.classic.joran.JoranConfigurator
|
||||
import ch.qos.logback.core.status.Status
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
import io.kotest.matchers.collections.shouldBeEmpty
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import java.nio.file.Path
|
||||
import javax.xml.XMLConstants
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.Node
|
||||
|
||||
class LogbackConfigurationTest : FunSpec({
|
||||
val logbackPath = Path.of(System.getProperty("user.dir"), "src/main/resources/logback.xml")
|
||||
|
||||
test("logback configuration is parseable and has one root") {
|
||||
val document = secureDocumentBuilderFactory()
|
||||
.newDocumentBuilder()
|
||||
.parse(logbackPath.toFile())
|
||||
|
||||
document.documentElement.tagName shouldBe "configuration"
|
||||
document.getElementsByTagName("configuration").length shouldBe 1
|
||||
document.childElements().map(Element::getTagName) shouldContainExactly listOf("configuration")
|
||||
}
|
||||
|
||||
test("console pattern is single-line and contains required structured fields") {
|
||||
val document = secureDocumentBuilderFactory()
|
||||
.newDocumentBuilder()
|
||||
.parse(logbackPath.toFile())
|
||||
val patterns = document.getElementsByTagName("pattern")
|
||||
|
||||
patterns.length shouldBe 1
|
||||
val pattern = patterns.item(0).textContent.trim()
|
||||
pattern.lines().size shouldBe 1
|
||||
listOf("time=", "level=", "logger=", "requestId=", "message=").forEach(pattern::shouldContain)
|
||||
pattern shouldContain "[REDACTED]"
|
||||
pattern shouldContain "%nopex"
|
||||
}
|
||||
|
||||
test("logback accepts the structured pattern without configuration errors") {
|
||||
val context = LoggerContext()
|
||||
|
||||
try {
|
||||
JoranConfigurator().apply { this.context = context }.doConfigure(logbackPath.toFile())
|
||||
|
||||
context.statusManager.copyOfStatusList
|
||||
.filter { it.level == Status.ERROR }
|
||||
.shouldBeEmpty()
|
||||
} finally {
|
||||
context.stop()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private fun secureDocumentBuilderFactory(): DocumentBuilderFactory =
|
||||
DocumentBuilderFactory.newInstance().apply {
|
||||
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
|
||||
setFeature("http://xml.org/sax/features/external-general-entities", false)
|
||||
setFeature("http://xml.org/sax/features/external-parameter-entities", false)
|
||||
setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "")
|
||||
setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "")
|
||||
isXIncludeAware = false
|
||||
isExpandEntityReferences = false
|
||||
}
|
||||
|
||||
private fun Node.childElements(): List<Element> =
|
||||
(0 until childNodes.length)
|
||||
.map(childNodes::item)
|
||||
.filterIsInstance<Element>()
|
||||
Reference in New Issue
Block a user