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:
@@ -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
|
||||
认证器 URI:otpauth://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"
|
||||
}
|
||||
Reference in New Issue
Block a user