Files
werkator/src/main/kotlin/de/hoennig/gittally/commands/ConfigPrintCommand.kt
T
mhoennigandClaude a4c995592f Header-only control token, masked secrets, loopback default (v0.9.9)
Finishes the small items of the security audit in
docs/prs/2026-07-08-PR#000: TODO 3, 4 and 7.

The three mutating endpoints of BuildsApiController no longer accept the
control token as a `token` query parameter — only the X-GitTally-Token
header, which the bundled UI has always used. URLs end up in access logs,
proxy logs, browser history and Referer headers, and the token never
expires, so a historical log capture would yield a valid credential.

`config:print` masks git.token as `***` on both the raw and the --full
path and names the new --show-secrets flag in a leading YAML comment, so
the output stays parseable when piped. The setup script points at
--show-secrets where it used to steer the operator to the plain token.

`server.bindAddress` now defaults to 127.0.0.1: neither the UI nor the
API authenticates read access, so reaching GitTally should require the
host's reverse proxy. Existing .gittally.yml files keep their explicit
value; the managed nginx container needs `0.0.0.0` set deliberately,
which is noted in the release notes, docs/configuration.md and
docs/deployment.md.

Released as v0.9.9, which also carries the previous two commits.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 07:38:33 +02:00

58 lines
2.0 KiB
Kotlin

package de.hoennig.gittally.commands
import de.hoennig.gittally.config.ConfigLoader
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import picocli.CommandLine.Option
@Component
@Command(
name = "config:print",
description = ["Print the effective configuration"],
mixinStandardHelpOptions = true,
)
class ConfigPrintCommand(
private val configLoader: ConfigLoader,
) : Runnable {
@Option(names = ["--full"], description = ["Include all defaults"])
var full: Boolean = false
@Option(names = ["--show-secrets"], description = ["Print secrets (git.token) in clear text instead of masked"])
var showSecrets: Boolean = false
override fun run() {
if (full) {
val config = configLoader.load()
printMaskingNote(config.git.token)
print(configLoader.toYaml(if (showSecrets) config else config.copy(git = config.git.copy(token = MASK))))
} else {
val raw = configLoader.loadRaw()
if (raw.isEmpty()) {
println("(no configuration files found)")
} else {
printMaskingNote(rawToken(raw))
print(configLoader.toYaml(if (showSecrets) raw else maskToken(raw)))
}
}
}
/** A YAML comment, so the output stays parseable when piped into a file. */
private fun printMaskingNote(token: String?) {
if (!showSecrets && !token.isNullOrEmpty()) {
println("# git.token is masked — pass --show-secrets to print it")
}
}
private fun rawToken(raw: Map<String, Any?>): String? = (raw["git"] as? Map<*, *>)?.get("token") as? String
private fun maskToken(raw: Map<String, Any?>): Map<String, Any?> {
val git = raw["git"] as? Map<*, *> ?: return raw
if (rawToken(raw).isNullOrEmpty()) return raw
return raw + ("git" to git.entries.associate { (key, value) -> key to if (key == "token") MASK else value })
}
companion object {
private const val MASK = "***"
}
}