Works off the security audit in docs/prs/2026-07-08-PR#000: TODO 1, 8, 9 and 10, the four items that need no design decision. New `SecretFiles` creates files holding secrets with mode 0600 and their directories with 0700 *at creation*, as a file attribute, instead of writing at the umask default and chmod-ing afterwards — that left a window in which the Gitea token was world-readable, which matters on a multi-tenant host. It is used by `init` for .git/gittally/.gittally.yml and by `ControlTokenService` for the control token; the shell setup script now writes its YAML in a `umask 077` subshell for the same reason. `ControlTokenService.matches` hashes both sides with SHA-256 before `MessageDigest.isEqual`, so the comparison always runs over two 32-byte buffers and cannot return early on a length mismatch. `GitService.checkout` and `fetchBranch` pass `--` before the refname, so a branch named like an option cannot be read as one. `resetHardToOrigin` keeps its plain form: `git reset --hard -- <commit>` is rejected outright and its argument is already `origin/`-prefixed. Co-Authored-By: Claude <noreply@anthropic.com>
52 lines
1.7 KiB
Kotlin
52 lines
1.7 KiB
Kotlin
package de.hoennig.gittally.server
|
|
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.matchers.shouldBe
|
|
import io.kotest.matchers.string.shouldMatch
|
|
import java.nio.file.Files
|
|
import java.nio.file.Path
|
|
import java.nio.file.attribute.PosixFilePermissions
|
|
|
|
class ControlTokenServiceTest : FunSpec() {
|
|
private fun newTokenFile(): Path = Files.createTempDirectory("gittally-token-test").resolve("control-token")
|
|
|
|
init {
|
|
test("generates a hex token once and persists it") {
|
|
val tokenFile = newTokenFile()
|
|
val service = ControlTokenService(tokenFile)
|
|
|
|
val token = service.token()
|
|
|
|
token shouldMatch Regex("[0-9a-f]{48}")
|
|
Files.readString(tokenFile).trim() shouldBe token
|
|
service.token() shouldBe token
|
|
}
|
|
|
|
test("persists the token readable only by the owner") {
|
|
val tokenFile = newTokenFile()
|
|
|
|
ControlTokenService(tokenFile).token()
|
|
|
|
PosixFilePermissions.toString(Files.getPosixFilePermissions(tokenFile)) shouldBe "rw-------"
|
|
}
|
|
|
|
test("reuses an operator-provided token file") {
|
|
val tokenFile = newTokenFile()
|
|
Files.createDirectories(tokenFile.parent)
|
|
Files.writeString(tokenFile, "my-own-token\n")
|
|
|
|
ControlTokenService(tokenFile).token() shouldBe "my-own-token"
|
|
}
|
|
|
|
test("matches only the exact token") {
|
|
val service = ControlTokenService(newTokenFile())
|
|
val token = service.token()
|
|
|
|
service.matches(token) shouldBe true
|
|
service.matches(token + "x") shouldBe false
|
|
service.matches("") shouldBe false
|
|
service.matches(null) shouldBe false
|
|
}
|
|
}
|
|
}
|