start adding signature and encryption

This commit is contained in:
gowthaman.b 2023-11-12 08:25:52 +05:30
parent 1a22043cf2
commit 10813529f2
9 changed files with 254 additions and 64 deletions

View File

@ -31,6 +31,8 @@ dependencies {
implementation("redis.clients:jedis:5.0.2") implementation("redis.clients:jedis:5.0.2")
implementation("org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.20") implementation("org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.20")
implementation("org.jetbrains.kotlin:kotlin-script-runtime:1.9.20") implementation("org.jetbrains.kotlin:kotlin-script-runtime:1.9.20")
implementation("org.bouncycastle:bcprov-jdk18on:1.76")
implementation("org.bouncycastle:bcpkix-jdk18on:1.76")
api ("net.cactusthorn.config:config-core:0.81") api ("net.cactusthorn.config:config-core:0.81")
kapt("net.cactusthorn.config:config-compiler:0.81") kapt("net.cactusthorn.config:config-compiler:0.81")
kapt("io.ebean:kotlin-querybean-generator:13.23.2") kapt("io.ebean:kotlin-querybean-generator:13.23.2")

View File

@ -1,6 +1,8 @@
package com.restapi package com.restapi
import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.config.Role
import com.restapi.config.Roles
import com.restapi.domain.EntityModel import com.restapi.domain.EntityModel
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.http.Handler import io.javalin.http.Handler

View File

@ -3,11 +3,15 @@ package com.restapi
import AuthTokenResponse import AuthTokenResponse
import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.restapi.config.Action
import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.config.Auth.getAuthEndpoint import com.restapi.config.Auth.getAuthEndpoint
import com.restapi.config.Auth.parseAuthToken import com.restapi.config.Auth.validateAuthToken
import com.restapi.config.Role
import com.restapi.config.Roles
import com.restapi.controllers.Entities import com.restapi.controllers.Entities
import com.restapi.domain.DataNotFoundException import com.restapi.domain.DataNotFoundException
import com.restapi.domain.Session
import com.restapi.domain.Session.objectMapper import com.restapi.domain.Session.objectMapper
import com.restapi.domain.Session.redis import com.restapi.domain.Session.redis
import com.restapi.domain.Session.setAuthorizedUser import com.restapi.domain.Session.setAuthorizedUser
@ -19,7 +23,6 @@ import io.javalin.http.*
import io.javalin.http.util.NaiveRateLimit import io.javalin.http.util.NaiveRateLimit
import io.javalin.http.util.RateLimitUtil import io.javalin.http.util.RateLimitUtil
import io.javalin.json.JavalinJackson import io.javalin.json.JavalinJackson
import io.javalin.security.RouteRole
import org.jose4j.jwt.consumer.InvalidJwtException import org.jose4j.jwt.consumer.InvalidJwtException
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.net.URI import java.net.URI
@ -34,6 +37,11 @@ import kotlin.jvm.optionals.getOrDefault
fun main(args: Array<String>) { fun main(args: Array<String>) {
val logger = LoggerFactory.getLogger("api") val logger = LoggerFactory.getLogger("api")
val adminRole = Role.Standard(Action.ADMIN)
val viewRole = Role.Standard(Action.VIEW)
val createRole = Role.Standard(Action.CREATE)
val updateRole = Role.Standard(Action.UPDATE)
val approveOrRejectRole = Role.Standard(Action.APPROVE)
//ratelimit based on IP Only //ratelimit based on IP Only
RateLimitUtil.keyFunction = { ctx -> ctx.header("X-Forwarded-For")?.split(",")?.get(0) ?: ctx.ip() } RateLimitUtil.keyFunction = { ctx -> ctx.header("X-Forwarded-For")?.split(",")?.get(0) ?: ctx.ip() }
@ -58,6 +66,7 @@ fun main(args: Array<String>) {
.routes { .routes {
path("/auth") { path("/auth") {
//for testing, development only
get("/init") { get("/init") {
val endpoint = getAuthEndpoint().authorizationEndpoint val endpoint = getAuthEndpoint().authorizationEndpoint
@ -110,18 +119,26 @@ fun main(args: Array<String>) {
?.replace("Bearer: ", "") ?.replace("Bearer: ", "")
?.trim() ?: throw UnauthorizedResponse() ?.trim() ?: throw UnauthorizedResponse()
setAuthorizedUser(parseAuthToken(authToken = authToken)) setAuthorizedUser(validateAuthToken(authToken = authToken))
if(appConfig.enforcePayloadEncryption()){
//todo: decrypt the request from user
}
}
after("/api/*") {
it.header("X-Signature", Session.sign(it.body()))
if(appConfig.enforcePayloadEncryption()){
//todo:, encrypt and set the response back to user
}
} }
val adminRole = Role.Standard(Action.ADMIN)
val viewRole = Role.Standard(Action.VIEW)
val createRole = Role.Standard(Action.CREATE)
val updateRole = Role.Standard(Action.UPDATE)
val approveOrRejectRole = Role.Standard(Action.APPROVE)
path("/api") { path("/api") {
post("/execute/{name}", Entities::executeStoredProcedure, Roles(adminRole, Role.DbOps)) post("/script/database/{name}", Entities::executeStoredProcedure, Roles(adminRole, Role.DbOps))
post("/script/{name}", Entities::executeScript, Roles(adminRole, Role.DbOps)) post("/script/{file}/{name}", Entities::executeScript, Roles(adminRole, Role.DbOps))
get("/{entity}/{id}", Entities::view, Roles(adminRole, viewRole)) get("/{entity}/{id}", Entities::view, Roles(adminRole, viewRole))
post("/{entity}/query/{id}", Entities::sqlQueryById, Roles(adminRole, viewRole)) post("/{entity}/query/{id}", Entities::sqlQueryById, Roles(adminRole, viewRole))
@ -191,23 +208,10 @@ fun main(args: Array<String>) {
} }
enum class Action {
CREATE, VIEW, UPDATE, DELETE, APPROVE, ADMIN
}
sealed class Role {
open class Standard(vararg val action: Action) : Role()
data object Entity : Role()
data object DbOps : Role()
}
open class Roles(vararg val roles: Role) : RouteRole
private fun getFormDataAsString(formData: Map<String, String>): String { private fun getFormDataAsString(formData: Map<String, String>): String {
val formBodyBuilder = StringBuilder() val formBodyBuilder = StringBuilder()
for ((key, value) in formData) { for ((key, value) in formData) {
if (formBodyBuilder.length > 0) { if (formBodyBuilder.isNotEmpty()) {
formBodyBuilder.append("&") formBodyBuilder.append("&")
} }
formBodyBuilder.append(URLEncoder.encode(key, StandardCharsets.UTF_8)) formBodyBuilder.append(URLEncoder.encode(key, StandardCharsets.UTF_8))

View File

@ -22,6 +22,10 @@ interface AppConfig {
@Default("false") @Default("false")
fun corsEnabled(): Boolean fun corsEnabled(): Boolean
@Key("app.name")
@Default("RestAPI")
fun appName(): String
@Key("app.port") @Key("app.port")
@Default("9001") @Default("9001")
fun portNumber(): Int fun portNumber(): Int
@ -73,9 +77,19 @@ interface AppConfig {
@Default("true") @Default("true")
fun enforceRoleRestriction(): Boolean fun enforceRoleRestriction(): Boolean
@Key("app.security.enforce_payload_encryption")
@Default("false")
fun enforcePayloadEncryption(): Boolean
@Key("app.scripts.path") @Key("app.scripts.path")
fun scriptsPath(): String fun scriptsPath(): String
@Key("app.security.private_key")
fun privateKey(): Optional<String>
@Key("app.security.public_key")
fun publicKey(): Optional<String>
companion object { companion object {
val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java) val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java)
} }

View File

@ -2,23 +2,16 @@ package com.restapi.config
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.domain.Session
import com.restapi.domain.Session.objectMapper import com.restapi.domain.Session.objectMapper
import io.javalin.http.Context import io.javalin.security.RouteRole
import io.javalin.http.HandlerType
import io.javalin.http.UnauthorizedResponse
import org.jose4j.jwk.HttpsJwks import org.jose4j.jwk.HttpsJwks
import org.jose4j.jwt.consumer.ErrorCodes
import org.jose4j.jwt.consumer.InvalidJwtException
import org.jose4j.jwt.consumer.JwtConsumerBuilder import org.jose4j.jwt.consumer.JwtConsumerBuilder
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver
import org.slf4j.LoggerFactory
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.net.http.HttpResponse import java.net.http.HttpResponse
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.function.Function
object Auth { object Auth {
@ -52,7 +45,7 @@ object Auth {
.build() .build()
fun parseAuthToken(authToken: String): AuthUser { fun validateAuthToken(authToken: String): AuthUser {
// Validate the JWT and process it to the Claims // Validate the JWT and process it to the Claims
val jwtClaims = jwtConsumer.process(authToken) val jwtClaims = jwtConsumer.process(authToken)
@ -70,3 +63,14 @@ object Auth {
} }
data class AuthUser(val userName: String, val tenant: String, val roles: List<String>) data class AuthUser(val userName: String, val tenant: String, val roles: List<String>)
enum class Action {
CREATE, VIEW, UPDATE, DELETE, APPROVE, ADMIN
}
sealed class Role {
open class Standard(vararg val action: Action) : Role()
data object Entity : Role()
data object DbOps : Role()
}
open class Roles(vararg val roles: Role) : RouteRole

View File

@ -1,9 +1,7 @@
package com.restapi.controllers package com.restapi.controllers
import com.restapi.domain.ApprovalStatus import com.restapi.domain.*
import com.restapi.domain.DataModel import com.restapi.domain.Session.currentUser
import com.restapi.domain.EntityModel
import com.restapi.domain.Session
import com.restapi.domain.Session.database import com.restapi.domain.Session.database
import com.restapi.domain.Session.findByEntityAndId import com.restapi.domain.Session.findByEntityAndId
import com.restapi.integ.Scripting import com.restapi.integ.Scripting
@ -14,17 +12,24 @@ import io.javalin.http.Context
import io.javalin.http.NotFoundResponse import io.javalin.http.NotFoundResponse
import io.javalin.http.bodyAsClass import io.javalin.http.bodyAsClass
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.sql.Types
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
data class PatchValue(val key: String, val value: Any)
data class Query( data class Query(
val sql: String, val sql: String,
val params: Map<String, Any> val params: Map<String, Any>
) )
enum class ResultType {
INTEGER, DECIMAL, STRING, DATETIME, ARRAY, OBJECT
}
data class RejectAction(val reason: String)
data class StoredProcedure(val input: Map<String, Any>, val output: Map<String, ResultType> = hashMapOf())
object Entities { object Entities {
private val logger = LoggerFactory.getLogger("Entities") private val logger = LoggerFactory.getLogger("Entities")
fun delete(ctx: Context) { fun delete(ctx: Context) {
@ -37,47 +42,90 @@ object Entities {
fun patch(ctx: Context) { fun patch(ctx: Context) {
val e = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id")) val e = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id"))
val pv = ctx.bodyAsClass<PatchValue>() val pv = ctx.bodyAsClass<Map<String, Any>>()
e.data[pv.key] = pv.value; pv.forEach { (key, value) ->
e.data[key] = value;
}
e.update() e.update()
} }
fun update(ctx: Context) { fun update(ctx: Context) {
val purgeExisting = ctx.queryParam("purge")?.toBooleanStrictOrNull() == true
val e = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id")) val e = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id"))
val newData = ctx.bodyAsClass<Map<String, Any>>() val newData = ctx.bodyAsClass<Map<String, Any>>()
if (purgeExisting) {
e.data.clear();
}
e.data.putAll(newData) e.data.putAll(newData)
e.update() e.update()
} }
fun action(ctx: Context) {} fun action(ctx: Context) {}
fun approve(ctx: Context) {} fun approve(ctx: Context) {
fun reject(ctx: Context) {} approveOrReject(ctx, ApprovalStatus.APPROVED)
}
fun reject(ctx: Context) {
approveOrReject(ctx, ApprovalStatus.REJECTED)
}
private fun approveOrReject(ctx: Context, rejected: ApprovalStatus) {
val e = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id"))
val reject = ctx.bodyAsClass<RejectAction>()
e.approvalStatus = rejected
e.comments.add(Comments(text = reject.reason, by = currentUser()))
e.save()
}
fun executeScript(ctx: Context) { fun executeScript(ctx: Context) {
val name = ctx.pathParam("name") val name = ctx.pathParam("name")
val file = ctx.pathParam("file")
val params = ctx.bodyAsClass<Map<String, Any>>() val params = ctx.bodyAsClass<Map<String, Any>>()
ctx.json( ctx.json(
Scripting. Scripting.execute(file, name, params)
execute(name, "execute", params, logger)
) )
} }
fun executeStoredProcedure(ctx: Context) { fun executeStoredProcedure(ctx: Context) {
val name = ctx.pathParam("name") val name = ctx.pathParam("name")
val params = ctx.bodyAsClass<Map<String, Any>>() val sp = ctx.bodyAsClass<StoredProcedure>()
val placeholders = (0..params.entries.size + 1).joinToString(",") { "?" }
val inputParams = sp.input.entries.toList()
val outputParams = sp.output.entries.toList()
val placeholders = (0..inputParams.size + 1).joinToString(",") { "?" }
val sql = "{call $name($placeholders)}" val sql = "{call $name($placeholders)}"
val cs: CallableSql = database.createCallableSql(sql) val cs: CallableSql = database.createCallableSql(sql)
params.entries.forEachIndexed { index, entry -> inputParams.forEachIndexed { index, entry ->
cs.setParameter(index + 1, entry.value) cs.setParameter(index + 1, entry.value)
} }
cs.setParameter(params.entries.size + 1, Session.currentTenant()) cs.setParameter(inputParams.size + 1, Session.currentTenant())
outputParams.forEachIndexed { idx, entry ->
when (entry.value) {
ResultType.INTEGER -> cs.registerOut(idx + 1, Types.INTEGER)
ResultType.DECIMAL -> cs.registerOut(idx + 1, Types.DOUBLE)
ResultType.STRING -> cs.registerOut(idx + 1, Types.VARCHAR)
ResultType.DATETIME -> cs.registerOut(idx + 1, Types.DATE)
ResultType.ARRAY -> cs.registerOut(idx + 1, Types.ARRAY)
ResultType.OBJECT -> cs.registerOut(idx + 1, Types.JAVA_OBJECT)
}
}
val done = database.execute(cs) val done = database.execute(cs)
val output = outputParams.mapIndexed { index, entry ->
Pair(entry.key, cs.getObject(index + 1))
}.toMap()
ctx.json( ctx.json(
mapOf( mapOf(
"done" to done "done" to done,
"output" to output
) )
) )
} }
@ -204,7 +252,7 @@ object Entities {
} }
} }
if (!setupEntity.preSaveScript.isNullOrEmpty()) { if (!setupEntity.preSaveScript.isNullOrEmpty()) {
val ok = Scripting.preSave(setupEntity.preSaveScript!!, "preSave", this, logger) as Boolean val ok = Scripting.execute(setupEntity.preSaveScript!!, "preSave", this) as Boolean
if (!ok) { if (!ok) {
throw BadRequestResponse("PreSave Failed") throw BadRequestResponse("PreSave Failed")
} }
@ -219,6 +267,10 @@ object Entities {
} }
} }
) )
if (setupEntity != null && !setupEntity.postSaveScript.isNullOrEmpty()) {
Scripting.execute(setupEntity.postSaveScript!!, "postSave", dataModel)
}
} }
private fun isValidDate(f: String) = try { private fun isValidDate(f: String) = try {

View File

@ -1,9 +1,7 @@
package com.restapi.domain package com.restapi.domain
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.config.AuthUser import com.restapi.config.AuthUser
@ -13,26 +11,138 @@ import io.ebean.config.CurrentTenantProvider
import io.ebean.config.CurrentUserProvider import io.ebean.config.CurrentUserProvider
import io.ebean.config.DatabaseConfig import io.ebean.config.DatabaseConfig
import io.ebean.config.TenantMode import io.ebean.config.TenantMode
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemReader
import org.jose4j.jwk.PublicJsonWebKey
import org.jose4j.jwk.RsaJsonWebKey
import org.jose4j.jwk.RsaJwkGenerator
import org.jose4j.jws.AlgorithmIdentifiers
import org.jose4j.jws.JsonWebSignature
import org.slf4j.LoggerFactory
import redis.clients.jedis.JedisPooled import redis.clients.jedis.JedisPooled
import java.io.StringReader
import java.io.StringWriter
import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.* import java.util.*
import kotlin.jvm.optionals.getOrDefault import kotlin.jvm.optionals.getOrDefault
object Session { object Session {
private val KEY_ID = "${appConfig.appName()}-KEY"
private val logger = LoggerFactory.getLogger("session")
private val currentUser = object : ThreadLocal<AuthUser>() { private val currentUser = object : ThreadLocal<AuthUser>() {
override fun initialValue(): AuthUser { override fun initialValue(): AuthUser {
return AuthUser("", "", emptyList<String>()) return AuthUser("", "", emptyList<String>())
} }
} }
fun setAuthorizedUser(a: AuthUser) = currentUser.set(a) fun setAuthorizedUser(a: AuthUser) = currentUser.set(a)
//if not passed in ENV, then we shall generate and print
private fun makeRsaJsonWebKey(publicKey: String, privateKey: String): RsaJsonWebKey {
val newPublicKey = readPublicKey(publicKey)
val newPrivateKey = readPrivateKey(privateKey)
val rsa = PublicJsonWebKey.Factory.newPublicJwk(newPublicKey) as RsaJsonWebKey
rsa.privateKey = newPrivateKey
rsa.keyId = KEY_ID
return rsa
}
private val keyFactory = KeyFactory.getInstance("RSA")
private fun readPublicKey(file: String): RSAPublicKey {
StringReader(file).use { keyReader ->
PemReader(keyReader).use { pemReader ->
val pemObject = pemReader.readPemObject()
val content = pemObject.content
val pubKeySpec = X509EncodedKeySpec(content)
return keyFactory.generatePublic(pubKeySpec) as RSAPublicKey
}
}
}
private fun readPrivateKey(file: String): RSAPrivateKey {
StringReader(file).use { keyReader ->
PemReader(keyReader).use { pemReader ->
val pemObject = pemReader.readPemObject()
val content = pemObject.content
val privKeySpec = PKCS8EncodedKeySpec(content)
return keyFactory.generatePrivate(privKeySpec) as RSAPrivateKey
}
}
}
private val keypair: RsaJsonWebKey by lazy {
if (appConfig.privateKey().isPresent && appConfig.publicKey().isPresent) {
makeRsaJsonWebKey(
appConfig.publicKey().get(),
appConfig.privateKey().get()
)
} else {
RsaJwkGenerator.generateJwk(2048).apply {
keyId = KEY_ID
logger.warn("Save this PrivateKey/PublicKey and pass it in the Config File from next time")
StringWriter().use { s ->
JcaPEMWriter(s).use { p ->
p.writeObject(privateKey)
}
logger.warn("Private Key\n${s.toString()}")
}
StringWriter().use { s ->
JcaPEMWriter(s).use { p ->
p.writeObject(publicKey)
}
logger.warn("Public Key\n${s.toString()}")
}
}
}
}
fun sign(payload: String): String {
// Create a new JsonWebSignature
val jws = JsonWebSignature()
// Set the payload, or signed content, on the JWS object
jws.setPayload(payload)
// Set the signature algorithm on the JWS that will integrity protect the payload
jws.algorithmHeaderValue = AlgorithmIdentifiers.RSA_PSS_USING_SHA512
// Set the signing key on the JWS
// Note that your application will need to determine where/how to get the key
// and here we just use an example from the JWS spec
jws.setKey(keypair.privateKey)
// Sign the JWS and produce the compact serialization or complete JWS representation, which
// is a string consisting of three dot ('.') separated base64url-encoded
// parts in the form Header.Payload.Signature
return jws.getCompactSerialization()
}
private val sc = DatabaseConfig().apply { private val sc = DatabaseConfig().apply {
loadFromProperties(Properties().apply { loadFromProperties(Properties().apply {
setProperty("datasource.db.username", appConfig.dbUser()) setProperty("datasource.db.username", appConfig.dbUser())
setProperty("datasource.db.password", appConfig.dbPass()) setProperty("datasource.db.password", appConfig.dbPass())
setProperty("datasource.db.url", appConfig.dbUrl()) setProperty("datasource.db.url", appConfig.dbUrl())
setProperty("ebean.migration.run", appConfig.dbRunMigration().toString()) setProperty("ebean.migration.run", appConfig.dbRunMigration().toString())
if(appConfig.seedSqlFile().isPresent){ if (appConfig.seedSqlFile().isPresent) {
setProperty("ebean.ddl.seedSql", appConfig.seedSqlFile().get()) setProperty("ebean.ddl.seedSql", appConfig.seedSqlFile().get())
} }
}) })

View File

@ -2,8 +2,10 @@ package com.restapi.integ
import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.domain.DataModel import com.restapi.domain.DataModel
import com.restapi.domain.Session import com.restapi.domain.Session.currentTenant
import org.slf4j.Logger import com.restapi.domain.Session.currentUser
import com.restapi.domain.Session.database
import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.script.Invocable import javax.script.Invocable
@ -12,18 +14,18 @@ import javax.script.ScriptEngineManager
object Scripting { object Scripting {
private val engineMap = ConcurrentHashMap<String, Invocable>() private val engineMap = ConcurrentHashMap<String, Invocable>()
private val logger = LoggerFactory.getLogger("script")
private fun getEngine(scriptName: String) = engineMap.computeIfAbsent(scriptName) { private fun getEngine(scriptName: String) = engineMap.computeIfAbsent(scriptName) {
val engine = ScriptEngineManager().getEngineByExtension("kts")!! val engine = ScriptEngineManager().getEngineByExtension("kts")!!
engine.eval(File(appConfig.scriptsPath(), scriptName).reader()) engine.eval(File(appConfig.scriptsPath(), scriptName).reader())
engine as Invocable engine as Invocable
} }
fun execute(scriptName: String, fnName: String, params: Map<String, Any>, logger: Logger): Any { fun execute(scriptName: String, fnName: String, params: Map<String, Any>): Any {
return getEngine(scriptName).invokeFunction(fnName, params, Session.database, logger) return getEngine(scriptName).invokeFunction(fnName, params, database, logger, currentUser(), currentTenant())
} }
fun preSave(scriptName: String, fnName: String, data: DataModel, logger: Logger): Any { fun execute(scriptName: String, fnName: String, dataModel: DataModel): Any {
return getEngine(scriptName).invokeFunction(fnName, data, Session.database, logger) return getEngine(scriptName).invokeFunction(fnName, dataModel, database, logger, currentUser(), currentTenant())
} }
} }

View File

@ -2,16 +2,16 @@ import com.restapi.domain.DataModel
import io.ebean.Database import io.ebean.Database
import org.slf4j.Logger import org.slf4j.Logger
fun execute(d: Map<String, Any>, db: Database, logger: Logger): Map<String, Any> { fun execute(d: Map<String, Any>, db: Database, logger: Logger, user: String, tenant: String): Map<String, Any> {
println("execute on $d") println("execute on $d")
return d return d
} }
fun preSave(d: DataModel, db: Database, logger: Logger): Boolean { fun preSave(d: DataModel, db: Database, logger: Logger, user: String, tenant: String): Boolean {
logger.warn("PreSave $d") logger.warn("PreSave $d")
return true return true
} }
fun postSave(d: DataModel, db: Database, logger: Logger) { fun postSave(d: DataModel, db: Database, logger: Logger, user: String, tenant: String) {
println("PostSave $d") println("PostSave $d")
} }