start adding signature and encryption
This commit is contained in:
parent
1a22043cf2
commit
10813529f2
@ -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")
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
@ -69,4 +62,15 @@ 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
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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")
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user