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:
@@ -3,6 +3,7 @@ package com.osglab.account.config
|
||||
import io.ktor.server.config.ApplicationConfig
|
||||
import java.net.URI
|
||||
import java.util.Base64
|
||||
import java.util.UUID
|
||||
|
||||
data class AppConfig(
|
||||
val environment: Environment,
|
||||
@@ -17,6 +18,7 @@ data class AppConfig(
|
||||
val credits: CreditsConfig,
|
||||
val providers: ProvidersConfig,
|
||||
val integrity: IntegrityConfig,
|
||||
val admin: AdminConfig = AdminConfig(),
|
||||
) {
|
||||
val isProduction: Boolean = environment == Environment.PRODUCTION
|
||||
|
||||
@@ -119,6 +121,41 @@ data class AppConfig(
|
||||
300,
|
||||
),
|
||||
)
|
||||
val adminEnabled = config.booleanOrDefault("app.admin.enabled", false)
|
||||
val adminBootstrapEnabled = config.booleanOrDefault(
|
||||
"app.admin.bootstrapEnabled",
|
||||
false,
|
||||
)
|
||||
require(!adminBootstrapEnabled || adminEnabled) {
|
||||
"app.admin.bootstrapEnabled requires app.admin.enabled"
|
||||
}
|
||||
val admin = AdminConfig(
|
||||
enabled = adminEnabled,
|
||||
bootstrapEnabled = adminBootstrapEnabled,
|
||||
bootstrapOperatorId = config.optionalValue("app.admin.bootstrapOperatorId")
|
||||
?.let {
|
||||
runCatching { UUID.fromString(it) }.getOrElse { cause ->
|
||||
throw ConfigValidationException(
|
||||
"app.admin.bootstrapOperatorId must be a UUID",
|
||||
cause,
|
||||
)
|
||||
}
|
||||
},
|
||||
bootstrapUsername = config.optionalValue("app.admin.bootstrapUsername"),
|
||||
bootstrapPasswordHash = config.optionalLiteralSecret(
|
||||
"app.admin.bootstrapPasswordHash",
|
||||
production && adminBootstrapEnabled,
|
||||
),
|
||||
bootstrapTotpSecretBase32 = config.optionalSecret(
|
||||
"app.admin.bootstrapTotpSecretBase32",
|
||||
production && adminBootstrapEnabled,
|
||||
),
|
||||
sessionHours = config.positiveLong("app.admin.sessionHours", 8),
|
||||
maximumManualGrant = config.positiveLong(
|
||||
"app.admin.maximumManualGrant",
|
||||
100_000,
|
||||
),
|
||||
)
|
||||
|
||||
require(session.hmacSecret.size >= MIN_HMAC_SECRET_BYTES) {
|
||||
"app.session.secret must contain at least $MIN_HMAC_SECRET_BYTES bytes"
|
||||
@@ -199,6 +236,26 @@ data class AppConfig(
|
||||
require(!production || apple.clientId == APP_ATTEST_BUNDLE_ID) {
|
||||
"Production Apple client ID must be $APP_ATTEST_BUNDLE_ID"
|
||||
}
|
||||
require(admin.sessionHours in 1..24) {
|
||||
"app.admin.sessionHours must be between 1 and 24"
|
||||
}
|
||||
require(admin.maximumManualGrant in 1..100_000_000) {
|
||||
"app.admin.maximumManualGrant must be between 1 and 100000000"
|
||||
}
|
||||
if (admin.bootstrapEnabled) {
|
||||
requireNotNull(admin.bootstrapOperatorId) {
|
||||
"app.admin.bootstrapOperatorId is required when admin bootstrap is enabled"
|
||||
}
|
||||
require(!admin.bootstrapUsername.isNullOrBlank()) {
|
||||
"app.admin.bootstrapUsername is required when admin bootstrap is enabled"
|
||||
}
|
||||
require(!admin.bootstrapPasswordHash.isNullOrBlank()) {
|
||||
"app.admin.bootstrapPasswordHash is required when admin bootstrap is enabled"
|
||||
}
|
||||
require(!admin.bootstrapTotpSecretBase32.isNullOrBlank()) {
|
||||
"app.admin.bootstrapTotpSecretBase32 is required when admin bootstrap is enabled"
|
||||
}
|
||||
}
|
||||
if (production) {
|
||||
requireExactAppleEndpoint(apple.jwksUrl, "/auth/keys", "JWKS")
|
||||
requireExactAppleEndpoint(apple.tokenUrl, "/auth/token", "token")
|
||||
@@ -239,6 +296,7 @@ data class AppConfig(
|
||||
credits = credits,
|
||||
providers = providers,
|
||||
integrity = integrity,
|
||||
admin = admin,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -335,6 +393,17 @@ data class IntegrityConfig(
|
||||
val appAttestBundleId: String = APP_ATTEST_BUNDLE_ID,
|
||||
)
|
||||
|
||||
data class AdminConfig(
|
||||
val enabled: Boolean = false,
|
||||
val bootstrapEnabled: Boolean = false,
|
||||
val bootstrapOperatorId: UUID? = null,
|
||||
val bootstrapUsername: String? = null,
|
||||
val bootstrapPasswordHash: String? = null,
|
||||
val bootstrapTotpSecretBase32: String? = null,
|
||||
val sessionHours: Long = 8,
|
||||
val maximumManualGrant: Long = 100_000,
|
||||
)
|
||||
|
||||
enum class IntegrityPolicy {
|
||||
MONITOR,
|
||||
ENFORCE;
|
||||
@@ -393,6 +462,19 @@ private fun ApplicationConfig.optionalSecret(path: String, production: Boolean):
|
||||
return value?.takeUnless(String::isPlaceholder)
|
||||
}
|
||||
|
||||
private fun ApplicationConfig.optionalLiteralSecret(path: String, production: Boolean): String? {
|
||||
val value = propertyOrNull(path)?.getString()?.trim()?.takeIf(String::isNotEmpty)
|
||||
val placeholder = value?.let {
|
||||
it.contains("replace-with", ignoreCase = true) ||
|
||||
it.contains("change-me", ignoreCase = true) ||
|
||||
it.contains("\${")
|
||||
} == true
|
||||
if (production && (value == null || placeholder)) {
|
||||
throw ConfigValidationException("Production secret is missing or uses a placeholder: $path")
|
||||
}
|
||||
return value?.takeUnless { placeholder }
|
||||
}
|
||||
|
||||
private fun String.isPlaceholder(): Boolean =
|
||||
PLACEHOLDER_MARKERS.any { marker -> contains(marker, ignoreCase = true) }
|
||||
|
||||
@@ -419,6 +501,15 @@ private fun ApplicationConfig.boolean(path: String): Boolean =
|
||||
}
|
||||
}
|
||||
|
||||
private fun ApplicationConfig.booleanOrDefault(path: String, default: Boolean): Boolean =
|
||||
propertyOrNull(path)?.getString()?.trim()?.takeIf(String::isNotEmpty)?.let {
|
||||
when (it.lowercase()) {
|
||||
"true" -> true
|
||||
"false" -> false
|
||||
else -> throw ConfigValidationException("$path must be true or false")
|
||||
}
|
||||
} ?: default
|
||||
|
||||
private fun ApplicationConfig.base64Key(path: String, production: Boolean): ByteArray {
|
||||
val encoded = secret(path, production)
|
||||
return try {
|
||||
|
||||
Reference in New Issue
Block a user