tighten the api
This commit is contained in:
@@ -26,7 +26,6 @@ class AppAccessManager : AccessManager {
|
||||
|
||||
override fun manage(handler: Handler, ctx: Context, routeRoles: Set<RouteRole>) {
|
||||
val pathParamMap = ctx.pathParamMap()
|
||||
logger.warn("access {}, {}", pathParamMap, routeRoles)
|
||||
val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$")
|
||||
|
||||
if (pathParamMap.values.count { !regex.matches(it) } > 0) {
|
||||
@@ -35,15 +34,16 @@ class AppAccessManager : AccessManager {
|
||||
val entity = pathParamMap["entity"]
|
||||
val action = pathParamMap["action"]
|
||||
|
||||
val allowedRoles = routeRoles.map { it as Role }.flatMap {
|
||||
when (it) {
|
||||
val allowedRoles = routeRoles.map { it as Roles }.flatMap { it.roles.toList() }.flatMap { role ->
|
||||
when (role) {
|
||||
Role.DbOps -> listOf("ROLE_DB_OPS")
|
||||
Role.Entity -> loadEntityActionRole(entity, action)
|
||||
is Role.Standard -> listOf("ROLE_${entity}_${it.action}")
|
||||
is Role.Standard -> role.action.toList().map { "ROLE_${entity}_${it}" }
|
||||
}.map(String::uppercase)
|
||||
}
|
||||
|
||||
val isAllowed = currentRoles().count { allowedRoles.contains(it) } > 0
|
||||
logger.warn("entity - $entity, action $action, userroles = ${currentRoles()}, allowed = $allowedRoles, isAllowed? $isAllowed, enforce? ${appConfig.enforceRoleRestriction()}")
|
||||
if (isAllowed || !appConfig.enforceRoleRestriction() || allowedRoles.isEmpty()) {
|
||||
//if role is allowed, or enforcement is turned off or no roles are explicitly allowed
|
||||
handler.handle(ctx)
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.restapi.domain.DataNotFoundException
|
||||
import com.restapi.domain.Session.objectMapper
|
||||
import com.restapi.domain.Session.redis
|
||||
import com.restapi.domain.Session.setAuthorizedUser
|
||||
import io.ebean.DataIntegrityException
|
||||
import io.ebean.DuplicateKeyException
|
||||
import io.javalin.Javalin
|
||||
import io.javalin.apibuilder.ApiBuilder.*
|
||||
@@ -19,6 +20,7 @@ import io.javalin.http.util.NaiveRateLimit
|
||||
import io.javalin.http.util.RateLimitUtil
|
||||
import io.javalin.json.JavalinJackson
|
||||
import io.javalin.security.RouteRole
|
||||
import org.jose4j.jwt.consumer.InvalidJwtException
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.URI
|
||||
import java.net.URLEncoder
|
||||
@@ -108,8 +110,6 @@ fun main(args: Array<String>) {
|
||||
?.replace("Bearer: ", "")
|
||||
?.trim() ?: throw UnauthorizedResponse()
|
||||
|
||||
logger.warn("authToken = $authToken")
|
||||
|
||||
setAuthorizedUser(parseAuthToken(authToken = authToken))
|
||||
}
|
||||
|
||||
@@ -139,34 +139,54 @@ fun main(args: Array<String>) {
|
||||
|
||||
|
||||
}
|
||||
.exception(DuplicateKeyException::class.java) { _, ctx ->
|
||||
.exception(DuplicateKeyException::class.java) { e, ctx ->
|
||||
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
|
||||
ctx.json(
|
||||
mapOf(
|
||||
"error" to "Duplicate Data"
|
||||
)
|
||||
).status(HttpStatus.CONFLICT)
|
||||
}
|
||||
.exception(DataNotFoundException::class.java) { _, ctx ->
|
||||
.exception(DataIntegrityException::class.java) { e, ctx ->
|
||||
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
|
||||
ctx.json(
|
||||
mapOf(
|
||||
"error" to "References Missing"
|
||||
)
|
||||
).status(HttpStatus.EXPECTATION_FAILED)
|
||||
}
|
||||
.exception(DataNotFoundException::class.java) { e, ctx ->
|
||||
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
|
||||
ctx.json(
|
||||
mapOf(
|
||||
"error" to "Data Not Found"
|
||||
)
|
||||
).status(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
.exception(IllegalArgumentException::class.java) { _, ctx ->
|
||||
.exception(IllegalArgumentException::class.java) { e, ctx ->
|
||||
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
|
||||
ctx.json(
|
||||
mapOf(
|
||||
"error" to "Incorrect Data"
|
||||
)
|
||||
).status(HttpStatus.BAD_REQUEST)
|
||||
}
|
||||
.exception(JsonMappingException::class.java) { _, ctx ->
|
||||
.exception(JsonMappingException::class.java) { e, ctx ->
|
||||
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
|
||||
ctx.json(
|
||||
mapOf(
|
||||
"error" to "Incorrect Data"
|
||||
)
|
||||
).status(HttpStatus.BAD_REQUEST)
|
||||
}
|
||||
.exception(InvalidJwtException::class.java) { e, ctx ->
|
||||
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
|
||||
ctx.json(
|
||||
mapOf(
|
||||
"error" to "Login required"
|
||||
)
|
||||
).status(HttpStatus.UNAUTHORIZED)
|
||||
}
|
||||
.start(appConfig.portNumber())
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ interface AppConfig {
|
||||
@Key("app.db.run_migration")
|
||||
fun dbRunMigration(): Boolean
|
||||
|
||||
@Key("app.db.seed_sql")
|
||||
fun seedSqlFile(): Optional<String>
|
||||
|
||||
@Key("app.iam.url")
|
||||
fun iamUrl(): String
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ object Entities {
|
||||
if (this.uniqueIdentifier.isEmpty()) {
|
||||
this.uniqueIdentifier = Session.nextUniqId(entity)
|
||||
}
|
||||
this.approvalStatus = ApprovalStatus.APPROVED
|
||||
}
|
||||
|
||||
database.save(
|
||||
|
||||
@@ -32,6 +32,9 @@ object Session {
|
||||
setProperty("datasource.db.password", appConfig.dbPass())
|
||||
setProperty("datasource.db.url", appConfig.dbUrl())
|
||||
setProperty("ebean.migration.run", appConfig.dbRunMigration().toString())
|
||||
if(appConfig.seedSqlFile().isPresent){
|
||||
setProperty("ebean.ddl.seedSql", appConfig.seedSqlFile().get())
|
||||
}
|
||||
})
|
||||
tenantMode = TenantMode.PARTITION
|
||||
currentTenantProvider = CurrentTenantProvider { currentUser.get().tenant }
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import io.ebean.Model
|
||||
import io.ebean.annotation.DbArray
|
||||
import io.ebean.annotation.DbDefault
|
||||
import io.ebean.annotation.DbJsonB
|
||||
import io.ebean.annotation.Index
|
||||
import io.ebean.annotation.Platform
|
||||
@@ -31,19 +32,22 @@ abstract class BaseModel : Model() {
|
||||
var sysPk: Long = 0
|
||||
|
||||
@SoftDelete
|
||||
@DbDefault("false")
|
||||
var deleted: Boolean = false
|
||||
|
||||
@Version
|
||||
@DbDefault("1")
|
||||
var version: Int = 0
|
||||
|
||||
@WhenCreated
|
||||
@DbDefault("now()")
|
||||
var createdAt: LocalDateTime = LocalDateTime.now()
|
||||
|
||||
@WhenModified
|
||||
@DbDefault("now()")
|
||||
var modifiedAt: LocalDateTime? = null
|
||||
|
||||
@TenantId
|
||||
var tenantId: String = ""
|
||||
|
||||
|
||||
@WhoCreated
|
||||
var createdBy: String = ""
|
||||
@@ -55,22 +59,34 @@ abstract class BaseModel : Model() {
|
||||
|
||||
var deletedBy: String? = null
|
||||
|
||||
@DbDefault("0")
|
||||
var currentApprovalLevel: Int = 0
|
||||
|
||||
@DbDefault("0")
|
||||
var requiredApprovalLevels: Int = 0
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
var approvalStatus: ApprovalStatus = ApprovalStatus.PENDING
|
||||
@DbDefault("APPROVED")
|
||||
var approvalStatus: ApprovalStatus = ApprovalStatus.APPROVED
|
||||
|
||||
@DbArray
|
||||
@DbDefault("{}")
|
||||
var tags: MutableList<String> = arrayListOf()
|
||||
|
||||
@DbJsonB
|
||||
@DbDefault("[]")
|
||||
var comments: MutableList<Comments> = arrayListOf()
|
||||
}
|
||||
|
||||
|
||||
@MappedSuperclass
|
||||
abstract class BaseTenantModel : BaseModel() {
|
||||
@TenantId
|
||||
var tenantId: String = ""
|
||||
}
|
||||
|
||||
@Entity
|
||||
open class TenantModel : BaseModel() {
|
||||
@Index(unique = true)
|
||||
var name: String = ""
|
||||
var domain: String = ""
|
||||
var mobile: List<String> = emptyList()
|
||||
@@ -86,7 +102,7 @@ enum class AuditType {
|
||||
|
||||
@Entity
|
||||
@Index(columnNames = ["audit_type", "entity", "unique_identifier", "tenant_id", "created_by"])
|
||||
open class AuditLog : BaseModel() {
|
||||
open class AuditLog : BaseTenantModel() {
|
||||
@Enumerated(EnumType.STRING)
|
||||
var auditType: AuditType = AuditType.CREATE
|
||||
|
||||
@@ -94,48 +110,54 @@ open class AuditLog : BaseModel() {
|
||||
var uniqueIdentifier: String = ""
|
||||
|
||||
@DbJsonB
|
||||
@Index(definition = "create index audit_log_values_idx on audit_log using GIN (data) ", platforms = [Platform.POSTGRES])
|
||||
@Index(definition = "create index audit_log_values_idx on audit_log using GIN (data)", platforms = [Platform.POSTGRES])
|
||||
var data: Map<String, Any> = hashMapOf()
|
||||
|
||||
@DbJsonB
|
||||
@Index(definition = "create index audit_log_changes_idx on audit_log using GIN (changes) ", platforms = [Platform.POSTGRES])
|
||||
@Index(definition = "create index audit_log_changes_idx on audit_log using GIN (changes)", platforms = [Platform.POSTGRES])
|
||||
var changes: Map<String, Any> = hashMapOf()
|
||||
}
|
||||
|
||||
@Entity
|
||||
open class EntityModel : BaseModel() {
|
||||
open class EntityModel : BaseTenantModel() {
|
||||
@Index(unique = true)
|
||||
@JsonDeserialize(using = SafeStringDeserializer::class)
|
||||
var name: String = ""
|
||||
|
||||
//a kts script that will return true/false along with errors before saving
|
||||
var preSaveScript: String = ""
|
||||
var preSaveScript: String? = ""
|
||||
|
||||
//a kts script that will do something ... returns void
|
||||
var postSaveScript: String = ""
|
||||
var postSaveScript: String? = ""
|
||||
|
||||
//this will create extra actions/roles in keycloak
|
||||
//the default actions are create, update, view, delete
|
||||
@DbArray
|
||||
@DbDefault("{}")
|
||||
var actions: List<String> = emptyList()
|
||||
|
||||
//allow only these fields, if this is empty, then all fields are allowed
|
||||
@DbArray
|
||||
@DbDefault("{}")
|
||||
var allowedFields: List<String> = emptyList()
|
||||
|
||||
//enforce field types, if this is present, only fields that are present is validated
|
||||
@DbJsonB
|
||||
@DbDefault("{}")
|
||||
var allowedFieldTypes: Map<String, String> = hashMapOf()
|
||||
|
||||
//when an entity is saved/updated audit logs will be populated, when this is empty, all fields are logged
|
||||
@DbArray
|
||||
@DbDefault("{}")
|
||||
var auditLogFields: List<String> = emptyList()
|
||||
|
||||
@DbJsonB
|
||||
@DbDefault("{}")
|
||||
var preferences: MutableMap<String, Any> = hashMapOf()
|
||||
|
||||
//if '0' then its auto saved, no approval steps are required, for further steps,
|
||||
//a user needs to have ROLE_ENTITY_APPROVE_LEVEL1, ROLE_ENTITY_APPROVE_LEVEL2 roles for further approvals
|
||||
@DbDefault("0")
|
||||
var approvalLevels: Int = 0
|
||||
}
|
||||
|
||||
@@ -148,7 +170,7 @@ enum class JobType {
|
||||
}
|
||||
|
||||
@Entity
|
||||
open class JobModel : BaseModel() {
|
||||
open class JobModel : BaseTenantModel() {
|
||||
@Index(unique = true)
|
||||
var jobName: String = ""
|
||||
|
||||
@@ -168,7 +190,7 @@ open class JobModel : BaseModel() {
|
||||
|
||||
@Entity
|
||||
@Index(unique = true, name = "entity_unique_id", columnNames = ["entity_name", "unique_identifier", "tenant_id"])
|
||||
open class DataModel : BaseModel() {
|
||||
open class DataModel : BaseTenantModel() {
|
||||
|
||||
@JsonDeserialize(using = SafeStringDeserializer::class)
|
||||
var uniqueIdentifier: String = ""
|
||||
|
||||
Reference in New Issue
Block a user