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:
@@ -7,6 +7,27 @@ import com.osglab.account.common.security.SessionJwt
|
||||
import com.osglab.account.common.security.installSessionAuthentication
|
||||
import com.osglab.account.config.AppConfig
|
||||
import com.osglab.account.config.DatabaseFactory
|
||||
import com.osglab.account.features.admin.grants.services.AdminGrantService
|
||||
import com.osglab.account.features.admin.repositories.AdminRepository
|
||||
import com.osglab.account.features.admin.repositories.ExposedAdminRepository
|
||||
import com.osglab.account.features.admin.routes.adminApiRoutes
|
||||
import com.osglab.account.features.admin.routes.adminWebRoutes
|
||||
import com.osglab.account.features.admin.security.AdminPasswordHasher
|
||||
import com.osglab.account.features.admin.security.AdminTotpVerifier
|
||||
import com.osglab.account.features.admin.security.BouncyCastleArgon2idPasswordHasher
|
||||
import com.osglab.account.features.admin.security.HmacTotpVerifier
|
||||
import com.osglab.account.features.admin.services.AdminAuthService
|
||||
import com.osglab.account.features.admin.services.AdminAuditService
|
||||
import com.osglab.account.features.admin.services.AdminBootstrapConfig
|
||||
import com.osglab.account.features.admin.services.AdminBootstrapService
|
||||
import com.osglab.account.features.admin.services.AdminOperatorService
|
||||
import com.osglab.account.features.admin.services.AdminSessionService
|
||||
import com.osglab.account.features.admin.stats.repositories.AdminStatsRepository
|
||||
import com.osglab.account.features.admin.stats.repositories.ExposedAdminStatsRepository
|
||||
import com.osglab.account.features.admin.stats.services.AdminStatsService
|
||||
import com.osglab.account.features.admin.users.repositories.AdminUsersRepository
|
||||
import com.osglab.account.features.admin.users.repositories.ExposedAdminUsersRepository
|
||||
import com.osglab.account.features.admin.users.services.AdminUsersService
|
||||
import com.osglab.account.features.account.AccountRepository
|
||||
import com.osglab.account.features.account.AccountReauthenticator
|
||||
import com.osglab.account.features.account.AccountService
|
||||
@@ -120,6 +141,7 @@ import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import org.koin.ktor.ext.getKoin
|
||||
@@ -180,6 +202,12 @@ fun Application.module() {
|
||||
register(PUBLIC_RATE_LIMIT) {
|
||||
rateLimiter(limit = 120, refillPeriod = 1.minutes)
|
||||
}
|
||||
register(ADMIN_AUTH_RATE_LIMIT) {
|
||||
rateLimiter(limit = 5, refillPeriod = 1.minutes)
|
||||
}
|
||||
register(ADMIN_API_RATE_LIMIT) {
|
||||
rateLimiter(limit = 60, refillPeriod = 1.minutes)
|
||||
}
|
||||
}
|
||||
installApiStatusPages()
|
||||
|
||||
@@ -191,6 +219,19 @@ fun Application.module() {
|
||||
val koin = getKoin()
|
||||
// Fail startup before accepting traffic if migrations or database connectivity fail.
|
||||
koin.get<DatabaseFactory>().database
|
||||
if (appConfig.admin.bootstrapEnabled) {
|
||||
runBlocking {
|
||||
koin.get<AdminBootstrapService>().initialize(
|
||||
AdminBootstrapConfig(
|
||||
enabled = true,
|
||||
operatorId = appConfig.admin.bootstrapOperatorId,
|
||||
username = appConfig.admin.bootstrapUsername,
|
||||
passwordHash = appConfig.admin.bootstrapPasswordHash,
|
||||
totpSecretBase32 = appConfig.admin.bootstrapTotpSecretBase32,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val sessionAuthenticator = koin.get<SessionAccessAuthenticator>()
|
||||
installSessionAuthentication(sessionAuthenticator::authenticate)
|
||||
val asrStreaming = if (appConfig.providers.volcengine.credentialsAvailable) {
|
||||
@@ -220,6 +261,15 @@ fun Application.module() {
|
||||
} catch (_: Exception) {
|
||||
// Durable settlement state is retried without logging provider data.
|
||||
}
|
||||
if (appConfig.admin.enabled) {
|
||||
try {
|
||||
koin.get<AdminSessionService>().cleanupInactive()
|
||||
} catch (exception: CancellationException) {
|
||||
throw exception
|
||||
} catch (_: Exception) {
|
||||
// Expired sessions are retried in bounded batches on the next cycle.
|
||||
}
|
||||
}
|
||||
delay(60_000)
|
||||
}
|
||||
}
|
||||
@@ -253,6 +303,21 @@ fun Application.module() {
|
||||
configureInviteWebRoutes(koin.get(), koin.get())
|
||||
integrityRoutes(koin.get())
|
||||
}
|
||||
if (appConfig.admin.enabled) {
|
||||
adminWebRoutes()
|
||||
rateLimit(ADMIN_API_RATE_LIMIT) {
|
||||
adminApiRoutes(
|
||||
config = appConfig,
|
||||
authService = koin.get(),
|
||||
sessionService = koin.get(),
|
||||
statsService = koin.get(),
|
||||
usersService = koin.get(),
|
||||
grantService = koin.get(),
|
||||
operatorService = koin.get(),
|
||||
auditService = koin.get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +361,33 @@ fun accountServerModule(config: AppConfig): Module = module {
|
||||
single { SessionJwt(config.session) }
|
||||
single { FieldEncryptor(config.encryption.key) }
|
||||
single { IdentityFingerprint(config.antiAbuse.identityHmacKey) }
|
||||
single<AdminRepository> { ExposedAdminRepository(get()) }
|
||||
single<AdminPasswordHasher> { BouncyCastleArgon2idPasswordHasher() }
|
||||
single<AdminTotpVerifier> { HmacTotpVerifier() }
|
||||
single {
|
||||
val dummyPassword = "invalid-admin-password-constant-work".toCharArray()
|
||||
try {
|
||||
AdminAuthService(
|
||||
repository = get(),
|
||||
passwordHasher = get(),
|
||||
dummyPasswordHash = get<AdminPasswordHasher>().hash(dummyPassword),
|
||||
totpVerifier = get(),
|
||||
fieldEncryptor = get(),
|
||||
sessionTtl = Duration.ofHours(config.admin.sessionHours),
|
||||
)
|
||||
} finally {
|
||||
dummyPassword.fill('\u0000')
|
||||
}
|
||||
}
|
||||
single { AdminSessionService(get()) }
|
||||
single { AdminBootstrapService(get(), get()) }
|
||||
single { AdminOperatorService(get(), get(), get()) }
|
||||
single { AdminAuditService(get()) }
|
||||
single<AdminStatsRepository> { ExposedAdminStatsRepository(get()) }
|
||||
single { AdminStatsService(get()) }
|
||||
single<AdminUsersRepository> { ExposedAdminUsersRepository(get()) }
|
||||
single { AdminUsersService(get()) }
|
||||
single { AdminGrantService(get()) }
|
||||
single<AppleJwksProvider> {
|
||||
RemoteAppleJwksProvider(get(), config.apple.jwksUrl)
|
||||
}
|
||||
@@ -513,3 +605,5 @@ private val AUTH_RATE_LIMIT = RateLimitName("auth")
|
||||
private val ACCOUNT_RATE_LIMIT = RateLimitName("account")
|
||||
private val GATEWAY_RATE_LIMIT = RateLimitName("gateway")
|
||||
private val PUBLIC_RATE_LIMIT = RateLimitName("public")
|
||||
private val ADMIN_AUTH_RATE_LIMIT = RateLimitName("admin-auth")
|
||||
private val ADMIN_API_RATE_LIMIT = RateLimitName("admin-api")
|
||||
|
||||
Reference in New Issue
Block a user