Add secure administrator operations console

Provide TOTP-authenticated, role-controlled user and credit workflows with paginated audit data and SQL-backed statistics so operations can manage growth safely.
This commit is contained in:
Rocky
2026-08-17 15:20:34 +08:00
parent 676bfd2451
commit 1a9c518f96
76 changed files with 14602 additions and 3 deletions
@@ -0,0 +1,104 @@
package com.osglab.account.tools
import com.osglab.account.features.admin.security.BouncyCastleArgon2idPasswordHasher
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption.CREATE_NEW
import java.nio.file.attribute.PosixFilePermission
import java.security.SecureRandom
import java.util.Base64
import java.util.UUID
/**
* Generates one bootstrap operator without printing credentials to stdout.
* Both output files are created with owner-only permissions and must not exist.
*/
object AdminCredentialGenerator {
@JvmStatic
fun main(arguments: Array<String>) {
require(arguments.size == 3) {
"Usage: <username> <runtime-env-output> <operator-handoff-output>"
}
val username = arguments[0].trim().lowercase()
require(USERNAME.matches(username)) { "Username must match ${USERNAME.pattern}" }
val runtimeOutput = Path.of(arguments[1]).toAbsolutePath()
val handoffOutput = Path.of(arguments[2]).toAbsolutePath()
require(runtimeOutput != handoffOutput) { "Output paths must be different" }
require(Files.notExists(runtimeOutput) && Files.notExists(handoffOutput)) {
"Output files must not already exist"
}
val random = SecureRandom()
val password = Base64.getUrlEncoder().withoutPadding()
.encodeToString(ByteArray(24).also(random::nextBytes))
val passwordChars = password.toCharArray()
val passwordHash = try {
BouncyCastleArgon2idPasswordHasher(secureRandom = random).hash(passwordChars)
} finally {
passwordChars.fill('\u0000')
}
val totpSecret = base32(ByteArray(20).also(random::nextBytes))
val operatorId = UUID.randomUUID()
writePrivate(
runtimeOutput,
"""
# One-time admin bootstrap values. Never commit, upload, or screenshot.
# After the first successful startup, set ADMIN_BOOTSTRAP_ENABLED=false
# and permanently remove all ADMIN_BOOTSTRAP_* credential values.
ADMIN_ENABLED=true
ADMIN_BOOTSTRAP_ENABLED=true
ADMIN_BOOTSTRAP_OPERATOR_ID=$operatorId
ADMIN_BOOTSTRAP_USERNAME=$username
ADMIN_BOOTSTRAP_PASSWORD_HASH='$passwordHash'
ADMIN_BOOTSTRAP_TOTP_SECRET_BASE32=$totpSecret
ADMIN_SESSION_HOURS=8
ADMIN_MAXIMUM_MANUAL_GRANT=100000
""".trimIndent() + "\n",
)
writePrivate(
handoffOutput,
"""
OSG 运营后台初始管理员
用户名:$username
密码:$password
TOTP 密钥:$totpSecret
认证器 URIotpauth://totp/OSG%20Admin:$username?secret=$totpSecret&issuer=OSG%20Admin&algorithm=SHA1&digits=6&period=30
仅保存在受信设备。首次部署成功后,请将密码录入密码管理器,并删除本文件。
""".trimIndent() + "\n",
)
}
private fun writePrivate(path: Path, content: String) {
Files.writeString(path, content, CREATE_NEW)
runCatching {
Files.setPosixFilePermissions(
path,
setOf(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE),
)
}
}
private fun base32(bytes: ByteArray): String {
val output = StringBuilder((bytes.size * 8 + 4) / 5)
var buffer = 0
var bufferedBits = 0
bytes.forEach { byte ->
buffer = (buffer shl 8) or (byte.toInt() and 0xff)
bufferedBits += 8
while (bufferedBits >= 5) {
bufferedBits -= 5
output.append(BASE32_ALPHABET[(buffer shr bufferedBits) and 0x1f])
buffer = if (bufferedBits == 0) 0 else buffer and ((1 shl bufferedBits) - 1)
}
}
if (bufferedBits > 0) {
output.append(BASE32_ALPHABET[(buffer shl (5 - bufferedBits)) and 0x1f])
}
return output.toString()
}
private val USERNAME = Regex("^[a-z0-9][a-z0-9._@-]{2,63}$")
private const val BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
}