diff --git a/src/main/kotlin/work/slhaf/hub/WebHost.kt b/src/main/kotlin/work/slhaf/hub/WebHost.kt index 074fe0c..f084200 100644 --- a/src/main/kotlin/work/slhaf/hub/WebHost.kt +++ b/src/main/kotlin/work/slhaf/hub/WebHost.kt @@ -1,394 +1,14 @@ package work.slhaf.hub -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty -import io.ktor.server.request.httpMethod -import io.ktor.server.request.path -import io.ktor.server.request.receiveText -import io.ktor.server.response.respondText -import io.ktor.server.routing.Routing -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.put -import io.ktor.server.routing.routing import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit -import org.slf4j.LoggerFactory import java.io.File private const val DEFAULT_PORT = 8080 private const val DEFAULT_SCRIPTS_DIR = "scripts" private const val DEFAULT_HOST = "0.0.0.0" private val DEFAULT_MAX_RUN_CONCURRENCY = Runtime.getRuntime().availableProcessors().coerceAtLeast(1) -private val requestLogger = LoggerFactory.getLogger("work.slhaf.hub.RequestAudit") - -private suspend inline fun withRequestAudit( - call: ApplicationCall, - endpoint: String, - authProvider: () -> AuthContext? = { null }, - crossinline block: suspend () -> Unit, -) { - val startNs = System.nanoTime() - var thrown: Throwable? = null - try { - block() - } catch (t: Throwable) { - thrown = t - throw t - } finally { - val durationMs = (System.nanoTime() - startNs) / 1_000_000 - val auth = authProvider() - val tokenType = auth?.type?.name?.lowercase() ?: "none" - val subToken = auth?.subTokenName ?: "-" - val script = call.parameters["script"] ?: "-" - val status = call.response.status()?.value ?: if (thrown == null) 200 else 500 - if (thrown == null) { - requestLogger.info( - "endpoint={} method={} path={} status={} durationMs={} tokenType={} subToken={} script={}", - endpoint, - call.request.httpMethod.value, - call.request.path(), - status, - durationMs, - tokenType, - subToken, - script, - ) - } else { - requestLogger.warn( - "endpoint={} method={} path={} status={} durationMs={} tokenType={} subToken={} script={} error={}", - endpoint, - call.request.httpMethod.value, - call.request.path(), - status, - durationMs, - tokenType, - subToken, - script, - "${thrown::class.simpleName}: ${thrown.message}", - ) - } - } -} - -private suspend fun handleSubTokenCreate(call: io.ktor.server.application.ApplicationCall, security: HostSecurity) { - val name = call.parameters["name"] - ?: return call.respondText("missing subtoken name", status = HttpStatusCode.BadRequest) - - if (!name.matches(Regex("[A-Za-z0-9._-]+"))) { - return call.respondText("invalid subtoken name", status = HttpStatusCode.BadRequest) - } - - val scriptsRaw = call.receiveText() - val scripts = - try { - parseScriptNameSet(scriptsRaw) - } catch (t: Throwable) { - return call.respondText(t.message ?: "invalid script names", status = HttpStatusCode.BadRequest) - } - - val created = - try { - security.subTokens.create(name, scripts) - } catch (t: Throwable) { - return call.respondText(t.message ?: "failed to create subtoken", status = HttpStatusCode.Conflict) - } - - call.respondText(subTokenItemJson(created, includeToken = true), contentType = ContentType.Application.Json, status = HttpStatusCode.Created) -} - -private suspend fun handleSubTokenUpdate(call: io.ktor.server.application.ApplicationCall, security: HostSecurity) { - val name = call.parameters["name"] - ?: return call.respondText("missing subtoken name", status = HttpStatusCode.BadRequest) - - val scriptsRaw = call.receiveText() - val scripts = - try { - parseScriptNameSet(scriptsRaw) - } catch (t: Throwable) { - return call.respondText(t.message ?: "invalid script names", status = HttpStatusCode.BadRequest) - } - - val updated = - try { - security.subTokens.update(name, scripts) - } catch (t: Throwable) { - return call.respondText(t.message ?: "failed to update subtoken", status = HttpStatusCode.NotFound) - } - - call.respondText(subTokenItemJson(updated, includeToken = true), contentType = ContentType.Application.Json) -} - -private suspend fun handleSubTokenGet(call: io.ktor.server.application.ApplicationCall, security: HostSecurity) { - val name = call.parameters["name"] - ?: return call.respondText("missing subtoken name", status = HttpStatusCode.BadRequest) - - val item = security.subTokens.get(name) - ?: return call.respondText("subtoken not found: $name", status = HttpStatusCode.NotFound) - - call.respondText(subTokenItemJson(item, includeToken = true), contentType = ContentType.Application.Json) -} - -private suspend fun handleSubTokenDelete(call: io.ktor.server.application.ApplicationCall, security: HostSecurity) { - val name = call.parameters["name"] - ?: return call.respondText("missing subtoken name", status = HttpStatusCode.BadRequest) - - val deleted = security.subTokens.delete(name) - if (!deleted) return call.respondText("subtoken not found: $name", status = HttpStatusCode.NotFound) - - call.respondText("deleted subtoken: $name") -} - -private suspend fun handleTypeForAuth(call: ApplicationCall, auth: AuthContext) { - call.respondText(tokenTypeJson(auth), contentType = ContentType.Application.Json) -} - -private suspend fun handleScriptsForAuth(call: ApplicationCall, scriptsDir: File, auth: AuthContext) { - val allow = visibleScriptsFor(auth) - call.respondText(renderScriptList(scriptsDir, allow), ContentType.Text.Plain) -} - -private suspend fun handleMetaForAuth(call: ApplicationCall, scriptsDir: File, auth: AuthContext) { - val name = call.parameters["script"] - ?: return call.respondText("missing route name", status = HttpStatusCode.BadRequest) - if (!requireScriptAccess(call, auth, name)) return - - val script = resolveScriptFile(scriptsDir, name) - ?: return call.respondText("invalid script name", status = HttpStatusCode.BadRequest) - if (!script.exists()) { - return call.respondText("script not found: ${script.name}", status = HttpStatusCode.NotFound) - } - - val (metadata, source) = loadMetadata(script) - call.respondText( - metadataJson(name, metadata, source), - contentType = ContentType.Application.Json, - ) -} - -private suspend fun handleRunForAuth( - call: ApplicationCall, - scriptsDir: File, - auth: AuthContext, - runConcurrencyLimiter: Semaphore, - consumeBody: Boolean, -) { - val name = call.parameters["script"] - ?: return call.respondText("missing route name", status = HttpStatusCode.BadRequest) - if (!requireScriptAccess(call, auth, name)) return - runConcurrencyLimiter.withPermit { - handleRunRequest(call, scriptsDir, consumeBody = consumeBody) - } -} - -private fun Routing.registerHeaderAuthenticatedRoutes( - scriptsDir: File, - security: HostSecurity, - runConcurrencyLimiter: Semaphore, -) { - get("/type") { - var authForLog: AuthContext? = null - withRequestAudit(call, "type", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - handleTypeForAuth(call, auth) - } - } - - get("/scripts") { - var authForLog: AuthContext? = null - withRequestAudit(call, "scripts.list", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - handleScriptsForAuth(call, scriptsDir, auth) - } - } - - get("/meta/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "meta.get", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - handleMetaForAuth(call, scriptsDir, auth) - } - } - - get("/run/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "run.get", { authForLog }) { - authForLog = requireAuth(call, security) ?: return@withRequestAudit - handleRunForAuth(call, scriptsDir, authForLog, runConcurrencyLimiter, consumeBody = false) - } - } - - post("/run/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "run.post", { authForLog }) { - authForLog = requireAuth(call, security) ?: return@withRequestAudit - handleRunForAuth(call, scriptsDir, authForLog, runConcurrencyLimiter, consumeBody = true) - } - } -} - -private fun Routing.registerSubTokenPathRoutes( - scriptsDir: File, - security: HostSecurity, - runConcurrencyLimiter: Semaphore, -) { - get("/u/{subAuth}/type") { - var authForLog: AuthContext? = null - withRequestAudit(call, "u.type", { authForLog }) { - val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit - authForLog = auth - handleTypeForAuth(call, auth) - } - } - - get("/u/{subAuth}/scripts") { - var authForLog: AuthContext? = null - withRequestAudit(call, "u.scripts.list", { authForLog }) { - val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit - authForLog = auth - handleScriptsForAuth(call, scriptsDir, auth) - } - } - - get("/u/{subAuth}/meta/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "u.meta.get", { authForLog }) { - val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit - authForLog = auth - handleMetaForAuth(call, scriptsDir, auth) - } - } - - get("/u/{subAuth}/run/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "u.run.get", { authForLog }) { - val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit - authForLog = auth - handleRunForAuth(call, scriptsDir, auth, runConcurrencyLimiter, consumeBody = false) - } - } - - post("/u/{subAuth}/run/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "u.run.post", { authForLog }) { - val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit - authForLog = auth - handleRunForAuth(call, scriptsDir, auth, runConcurrencyLimiter, consumeBody = true) - } - } -} - -fun Application.webModule(scriptsDir: File, security: HostSecurity, runConcurrencyLimiter: Semaphore) { - routing { - get("/health") { - withRequestAudit(call, "health") { - call.respondText("OK") - } - } - - registerHeaderAuthenticatedRoutes(scriptsDir, security, runConcurrencyLimiter) - registerSubTokenPathRoutes(scriptsDir, security, runConcurrencyLimiter) - - get("/scripts/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "scripts.get", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - if (!requireRoot(call, auth)) return@withRequestAudit - handleGetScriptContent(call, scriptsDir) - } - } - - post("/scripts/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "scripts.create", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - if (!requireRoot(call, auth)) return@withRequestAudit - handleCreateScript(call, scriptsDir) - } - } - - put("/scripts/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "scripts.update", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - if (!requireRoot(call, auth)) return@withRequestAudit - handleUpdateScript(call, scriptsDir) - } - } - - delete("/scripts/{script}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "scripts.delete", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - if (!requireRoot(call, auth)) return@withRequestAudit - handleDeleteScript(call, scriptsDir) - } - } - - get("/subtokens") { - var authForLog: AuthContext? = null - withRequestAudit(call, "subtokens.list", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - if (!requireRoot(call, auth)) return@withRequestAudit - call.respondText(subTokenListJson(security.subTokens.list()), contentType = ContentType.Application.Json) - } - } - - get("/subtokens/{name}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "subtokens.get", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - if (!requireRoot(call, auth)) return@withRequestAudit - handleSubTokenGet(call, security) - } - } - - post("/subtokens/{name}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "subtokens.create", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - if (!requireRoot(call, auth)) return@withRequestAudit - handleSubTokenCreate(call, security) - } - } - - put("/subtokens/{name}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "subtokens.update", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - if (!requireRoot(call, auth)) return@withRequestAudit - handleSubTokenUpdate(call, security) - } - } - - delete("/subtokens/{name}") { - var authForLog: AuthContext? = null - withRequestAudit(call, "subtokens.delete", { authForLog }) { - val auth = requireAuth(call, security) ?: return@withRequestAudit - authForLog = auth - if (!requireRoot(call, auth)) return@withRequestAudit - handleSubTokenDelete(call, security) - } - } - } -} private fun usage() { println( diff --git a/src/main/kotlin/work/slhaf/hub/WebRoutes.kt b/src/main/kotlin/work/slhaf/hub/WebRoutes.kt new file mode 100644 index 0000000..a86e0e9 --- /dev/null +++ b/src/main/kotlin/work/slhaf/hub/WebRoutes.kt @@ -0,0 +1,387 @@ +package work.slhaf.hub + +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call +import io.ktor.server.request.httpMethod +import io.ktor.server.request.path +import io.ktor.server.request.receiveText +import io.ktor.server.response.respondText +import io.ktor.server.routing.Routing +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.put +import io.ktor.server.routing.routing +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import org.slf4j.LoggerFactory +import java.io.File + +private val requestLogger = LoggerFactory.getLogger("work.slhaf.hub.RequestAudit") + +private suspend inline fun withRequestAudit( + call: ApplicationCall, + endpoint: String, + authProvider: () -> AuthContext? = { null }, + crossinline block: suspend () -> Unit, +) { + val startNs = System.nanoTime() + var thrown: Throwable? = null + try { + block() + } catch (t: Throwable) { + thrown = t + throw t + } finally { + val durationMs = (System.nanoTime() - startNs) / 1_000_000 + val auth = authProvider() + val tokenType = auth?.type?.name?.lowercase() ?: "none" + val subToken = auth?.subTokenName ?: "-" + val script = call.parameters["script"] ?: "-" + val status = call.response.status()?.value ?: if (thrown == null) 200 else 500 + if (thrown == null) { + requestLogger.info( + "endpoint={} method={} path={} status={} durationMs={} tokenType={} subToken={} script={}", + endpoint, + call.request.httpMethod.value, + call.request.path(), + status, + durationMs, + tokenType, + subToken, + script, + ) + } else { + requestLogger.warn( + "endpoint={} method={} path={} status={} durationMs={} tokenType={} subToken={} script={} error={}", + endpoint, + call.request.httpMethod.value, + call.request.path(), + status, + durationMs, + tokenType, + subToken, + script, + "${thrown::class.simpleName}: ${thrown.message}", + ) + } + } +} + +private suspend fun handleSubTokenCreate(call: ApplicationCall, security: HostSecurity) { + val name = call.parameters["name"] + ?: return call.respondText("missing subtoken name", status = HttpStatusCode.BadRequest) + + if (!name.matches(Regex("[A-Za-z0-9._-]+"))) { + return call.respondText("invalid subtoken name", status = HttpStatusCode.BadRequest) + } + + val scriptsRaw = call.receiveText() + val scripts = + try { + parseScriptNameSet(scriptsRaw) + } catch (t: Throwable) { + return call.respondText(t.message ?: "invalid script names", status = HttpStatusCode.BadRequest) + } + + val created = + try { + security.subTokens.create(name, scripts) + } catch (t: Throwable) { + return call.respondText(t.message ?: "failed to create subtoken", status = HttpStatusCode.Conflict) + } + + call.respondText(subTokenItemJson(created, includeToken = true), contentType = ContentType.Application.Json, status = HttpStatusCode.Created) +} + +private suspend fun handleSubTokenUpdate(call: ApplicationCall, security: HostSecurity) { + val name = call.parameters["name"] + ?: return call.respondText("missing subtoken name", status = HttpStatusCode.BadRequest) + + val scriptsRaw = call.receiveText() + val scripts = + try { + parseScriptNameSet(scriptsRaw) + } catch (t: Throwable) { + return call.respondText(t.message ?: "invalid script names", status = HttpStatusCode.BadRequest) + } + + val updated = + try { + security.subTokens.update(name, scripts) + } catch (t: Throwable) { + return call.respondText(t.message ?: "failed to update subtoken", status = HttpStatusCode.NotFound) + } + + call.respondText(subTokenItemJson(updated, includeToken = true), contentType = ContentType.Application.Json) +} + +private suspend fun handleSubTokenGet(call: ApplicationCall, security: HostSecurity) { + val name = call.parameters["name"] + ?: return call.respondText("missing subtoken name", status = HttpStatusCode.BadRequest) + + val item = security.subTokens.get(name) + ?: return call.respondText("subtoken not found: $name", status = HttpStatusCode.NotFound) + + call.respondText(subTokenItemJson(item, includeToken = true), contentType = ContentType.Application.Json) +} + +private suspend fun handleSubTokenDelete(call: ApplicationCall, security: HostSecurity) { + val name = call.parameters["name"] + ?: return call.respondText("missing subtoken name", status = HttpStatusCode.BadRequest) + + val deleted = security.subTokens.delete(name) + if (!deleted) return call.respondText("subtoken not found: $name", status = HttpStatusCode.NotFound) + + call.respondText("deleted subtoken: $name") +} + +private suspend fun handleTypeForAuth(call: ApplicationCall, auth: AuthContext) { + call.respondText(tokenTypeJson(auth), contentType = ContentType.Application.Json) +} + +private suspend fun handleScriptsForAuth(call: ApplicationCall, scriptsDir: File, auth: AuthContext) { + val allow = visibleScriptsFor(auth) + call.respondText(renderScriptList(scriptsDir, allow), ContentType.Text.Plain) +} + +private suspend fun handleMetaForAuth(call: ApplicationCall, scriptsDir: File, auth: AuthContext) { + val name = call.parameters["script"] + ?: return call.respondText("missing route name", status = HttpStatusCode.BadRequest) + if (!requireScriptAccess(call, auth, name)) return + + val script = resolveScriptFile(scriptsDir, name) + ?: return call.respondText("invalid script name", status = HttpStatusCode.BadRequest) + if (!script.exists()) { + return call.respondText("script not found: ${script.name}", status = HttpStatusCode.NotFound) + } + + val (metadata, source) = loadMetadata(script) + call.respondText( + metadataJson(name, metadata, source), + contentType = ContentType.Application.Json, + ) +} + +private suspend fun handleRunForAuth( + call: ApplicationCall, + scriptsDir: File, + auth: AuthContext, + runConcurrencyLimiter: Semaphore, + consumeBody: Boolean, +) { + val name = call.parameters["script"] + ?: return call.respondText("missing route name", status = HttpStatusCode.BadRequest) + if (!requireScriptAccess(call, auth, name)) return + runConcurrencyLimiter.withPermit { + handleRunRequest(call, scriptsDir, consumeBody = consumeBody) + } +} + +private fun Routing.registerHeaderAuthenticatedRoutes( + scriptsDir: File, + security: HostSecurity, + runConcurrencyLimiter: Semaphore, +) { + get("/type") { + var authForLog: AuthContext? = null + withRequestAudit(call, "type", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleTypeForAuth(call, auth) + } + } + + get("/scripts") { + var authForLog: AuthContext? = null + withRequestAudit(call, "scripts.list", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleScriptsForAuth(call, scriptsDir, auth) + } + } + + get("/meta/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "meta.get", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleMetaForAuth(call, scriptsDir, auth) + } + } + + get("/run/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "run.get", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleRunForAuth(call, scriptsDir, auth, runConcurrencyLimiter, consumeBody = false) + } + } + + post("/run/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "run.post", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleRunForAuth(call, scriptsDir, auth, runConcurrencyLimiter, consumeBody = true) + } + } +} + +private fun Routing.registerSubTokenPathRoutes( + scriptsDir: File, + security: HostSecurity, + runConcurrencyLimiter: Semaphore, +) { + get("/u/{subAuth}/type") { + var authForLog: AuthContext? = null + withRequestAudit(call, "u.type", { authForLog }) { + val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleTypeForAuth(call, auth) + } + } + + get("/u/{subAuth}/scripts") { + var authForLog: AuthContext? = null + withRequestAudit(call, "u.scripts.list", { authForLog }) { + val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleScriptsForAuth(call, scriptsDir, auth) + } + } + + get("/u/{subAuth}/meta/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "u.meta.get", { authForLog }) { + val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleMetaForAuth(call, scriptsDir, auth) + } + } + + get("/u/{subAuth}/run/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "u.run.get", { authForLog }) { + val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleRunForAuth(call, scriptsDir, auth, runConcurrencyLimiter, consumeBody = false) + } + } + + post("/u/{subAuth}/run/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "u.run.post", { authForLog }) { + val auth = requireSubTokenPathAuth(call, security) ?: return@withRequestAudit + authForLog = auth + handleRunForAuth(call, scriptsDir, auth, runConcurrencyLimiter, consumeBody = true) + } + } +} + +fun Application.webModule(scriptsDir: File, security: HostSecurity, runConcurrencyLimiter: Semaphore) { + routing { + get("/health") { + withRequestAudit(call, "health") { + call.respondText("OK") + } + } + + registerHeaderAuthenticatedRoutes(scriptsDir, security, runConcurrencyLimiter) + registerSubTokenPathRoutes(scriptsDir, security, runConcurrencyLimiter) + + get("/scripts/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "scripts.get", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + if (!requireRoot(call, auth)) return@withRequestAudit + handleGetScriptContent(call, scriptsDir) + } + } + + post("/scripts/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "scripts.create", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + if (!requireRoot(call, auth)) return@withRequestAudit + handleCreateScript(call, scriptsDir) + } + } + + put("/scripts/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "scripts.update", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + if (!requireRoot(call, auth)) return@withRequestAudit + handleUpdateScript(call, scriptsDir) + } + } + + delete("/scripts/{script}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "scripts.delete", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + if (!requireRoot(call, auth)) return@withRequestAudit + handleDeleteScript(call, scriptsDir) + } + } + + get("/subtokens") { + var authForLog: AuthContext? = null + withRequestAudit(call, "subtokens.list", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + if (!requireRoot(call, auth)) return@withRequestAudit + call.respondText(subTokenListJson(security.subTokens.list()), contentType = ContentType.Application.Json) + } + } + + get("/subtokens/{name}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "subtokens.get", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + if (!requireRoot(call, auth)) return@withRequestAudit + handleSubTokenGet(call, security) + } + } + + post("/subtokens/{name}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "subtokens.create", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + if (!requireRoot(call, auth)) return@withRequestAudit + handleSubTokenCreate(call, security) + } + } + + put("/subtokens/{name}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "subtokens.update", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + if (!requireRoot(call, auth)) return@withRequestAudit + handleSubTokenUpdate(call, security) + } + } + + delete("/subtokens/{name}") { + var authForLog: AuthContext? = null + withRequestAudit(call, "subtokens.delete", { authForLog }) { + val auth = requireAuth(call, security) ?: return@withRequestAudit + authForLog = auth + if (!requireRoot(call, auth)) return@withRequestAudit + handleSubTokenDelete(call, security) + } + } + } +}