From 31388bae59dbc97e9d7a987f16ca9f6b7f9c1ab0 Mon Sep 17 00:00:00 2001 From: "gowthaman.b" Date: Sat, 11 Nov 2023 13:10:25 +0530 Subject: [PATCH] more optional validations --- .../kotlin/com/restapi/AppAccessManager.kt | 41 +++++- src/main/kotlin/com/restapi/Main.kt | 45 +++--- .../kotlin/com/restapi/config/AppConfig.kt | 7 + .../com/restapi/controllers/Entities.kt | 138 ++++++++++++++++-- src/main/kotlin/com/restapi/domain/db.kt | 1 + src/main/kotlin/com/restapi/domain/models.kt | 23 ++- .../kotlin/com/restapi/integ/Scripting.kt | 38 +---- .../resources/dbmigration/1.0__initial.sql | 117 ++++++++++++++- .../dbmigration/model/1.0__initial.model.xml | 99 +++++++++++++ 9 files changed, 441 insertions(+), 68 deletions(-) diff --git a/src/main/kotlin/com/restapi/AppAccessManager.kt b/src/main/kotlin/com/restapi/AppAccessManager.kt index b84e445..f1b7498 100644 --- a/src/main/kotlin/com/restapi/AppAccessManager.kt +++ b/src/main/kotlin/com/restapi/AppAccessManager.kt @@ -1,22 +1,55 @@ package com.restapi +import com.restapi.config.AppConfig.Companion.appConfig +import com.restapi.domain.EntityModel import io.javalin.http.Context import io.javalin.http.Handler import io.javalin.http.HttpStatus import io.javalin.security.AccessManager import io.javalin.security.RouteRole import org.slf4j.LoggerFactory +import com.restapi.domain.Session.currentRoles +import com.restapi.domain.Session.database class AppAccessManager : AccessManager { private val logger = LoggerFactory.getLogger("Access") + private fun loadEntityActionRole(entity: String?, action: String?): List { + if (entity == null || action == null) return emptyList() + + return database.find(EntityModel::class.java) + .where() + .eq("name", entity) + .findOne()?.actions + ?.filter { it.equals(action, ignoreCase = true) } + ?.map { "role_${entity}_$it" } ?: emptyList() + } + override fun manage(handler: Handler, ctx: Context, routeRoles: Set) { - logger.warn("access {}, {}", ctx.pathParamMap(), routeRoles) + val pathParamMap = ctx.pathParamMap() + logger.warn("access {}, {}", pathParamMap, routeRoles) val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$") - if(ctx.pathParamMap().values.count { !regex.matches(it) } > 0){ - ctx.status(HttpStatus.FORBIDDEN).result("invalid request") + if (pathParamMap.values.count { !regex.matches(it) } > 0) { + ctx.status(HttpStatus.FORBIDDEN).result("invalid request") } else { - handler.handle(ctx) + val entity = pathParamMap["entity"] + val action = pathParamMap["action"] + + val allowedRoles = routeRoles.map { it as Role }.flatMap { + when (it) { + Role.DbOps -> listOf("ROLE_DB_OPS") + Role.Entity -> loadEntityActionRole(entity, action) + is Role.Standard -> listOf("ROLE_${entity}_${it.action}") + }.map(String::uppercase) + } + + val isAllowed = currentRoles().count { allowedRoles.contains(it) } > 0 + if (isAllowed || !appConfig.enforceRoleRestriction() || allowedRoles.isEmpty()) { + //if role is allowed, or enforcement is turned off or no roles are explicitly allowed + handler.handle(ctx) + } else { + ctx.status(HttpStatus.UNAUTHORIZED).result("unauthorized request") + } } } } \ No newline at end of file diff --git a/src/main/kotlin/com/restapi/Main.kt b/src/main/kotlin/com/restapi/Main.kt index 9b9594e..fd63bdd 100644 --- a/src/main/kotlin/com/restapi/Main.kt +++ b/src/main/kotlin/com/restapi/Main.kt @@ -8,11 +8,9 @@ import com.restapi.config.Auth.getAuthEndpoint import com.restapi.config.Auth.parseAuthToken import com.restapi.controllers.Entities import com.restapi.domain.DataNotFoundException -import com.restapi.domain.Session.database import com.restapi.domain.Session.objectMapper import com.restapi.domain.Session.redis import com.restapi.domain.Session.setAuthorizedUser -import io.ebean.CallableSql import io.ebean.DuplicateKeyException import io.javalin.Javalin import io.javalin.apibuilder.ApiBuilder.* @@ -20,7 +18,6 @@ import io.javalin.http.* import io.javalin.http.util.NaiveRateLimit import io.javalin.http.util.RateLimitUtil import io.javalin.json.JavalinJackson -import io.javalin.security.AccessManager import io.javalin.security.RouteRole import org.slf4j.LoggerFactory import java.net.URI @@ -99,11 +96,15 @@ fun main(args: Array) { } before("/api/*") { ctx -> - //validate, auth token - NaiveRateLimit.requestPerTimeUnit(ctx, appConfig.rateLimit().getOrDefault(30), TimeUnit.MINUTES) // throws if rate limit is exceeded + NaiveRateLimit.requestPerTimeUnit( + ctx, + appConfig.rateLimit().getOrDefault(30), + TimeUnit.MINUTES + ) - val authToken = ctx.header("Authorization")?.replace("Bearer ", "") + val authToken = ctx.header("Authorization") + ?.replace("Bearer ", "") ?.replace("Bearer: ", "") ?.trim() ?: throw UnauthorizedResponse() @@ -111,21 +112,29 @@ fun main(args: Array) { setAuthorizedUser(parseAuthToken(authToken = authToken)) } + + 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") { - post("/execute/{name}", Entities::executeStoredProcedure, Roles(Role.DbOps)) + post("/execute/{name}", Entities::executeStoredProcedure, Roles(adminRole, Role.DbOps)) + post("/script/{name}", Entities::executeScript, Roles(adminRole, Role.DbOps)) - get("/{entity}/{id}", Entities::view, Roles(Role.Standard(Action.VIEW))) - post("/{entity}/query/{id}", Entities::sqlQueryId, Roles(Role.Standard(Action.VIEW))) - post("/{entity}/query", Entities::sqlQueryRaw, Roles(Role.Standard(Action.VIEW))) - post("/{entity}", Entities::create, Roles(Role.Standard(Action.CREATE))) + get("/{entity}/{id}", Entities::view, Roles(adminRole, viewRole)) + post("/{entity}/query/{id}", Entities::sqlQueryById, Roles(adminRole, viewRole)) + post("/{entity}/query", Entities::sqlQueryRaw, Roles(adminRole, viewRole)) + post("/{entity}", Entities::create, Roles(adminRole, createRole)) - put("/{entity}/approve/{id}", Entities::approve, Roles(Role.Standard(Action.APPROVE))) - put("/{entity}/reject/{id}", Entities::reject, Roles(Role.Standard(Action.APPROVE))) - put("/{entity}/{action}/{id}", Entities::action, Roles(Role.Entity)) + put("/{entity}/approve/{id}", Entities::approve, Roles(adminRole, approveOrRejectRole)) + put("/{entity}/reject/{id}", Entities::reject, Roles(adminRole, approveOrRejectRole)) + put("/{entity}/{action}/{id}", Entities::action, Roles(adminRole, Role.Entity)) - put("/{entity}/{id}", Entities::update, Roles(Role.Standard(Action.UPDATE))) - patch("/{entity}/{id}", Entities::patch, Roles(Role.Standard(Action.UPDATE))) - delete("/{entity}/{id}", Entities::delete, Roles(Role.Standard(Action.DELETE))) + put("/{entity}/{id}", Entities::update, Roles(adminRole, updateRole)) + patch("/{entity}/{id}", Entities::patch, Roles(adminRole, updateRole)) + delete("/{entity}/{id}", Entities::delete, Roles(adminRole, Role.Standard(Action.DELETE))) } @@ -163,7 +172,7 @@ fun main(args: Array) { enum class Action { - CREATE, VIEW, UPDATE, DELETE, APPROVE + CREATE, VIEW, UPDATE, DELETE, APPROVE, ADMIN } sealed class Role { diff --git a/src/main/kotlin/com/restapi/config/AppConfig.kt b/src/main/kotlin/com/restapi/config/AppConfig.kt index 7c0c2fe..c1a90e1 100644 --- a/src/main/kotlin/com/restapi/config/AppConfig.kt +++ b/src/main/kotlin/com/restapi/config/AppConfig.kt @@ -66,6 +66,13 @@ interface AppConfig { @Key("app.network.rate_limit") fun rateLimit(): Optional + @Key("app.security.enforce_role_restriction") + @Default("true") + fun enforceRoleRestriction(): Boolean + + @Key("app.scripts.path") + fun scriptsPath(): String + companion object { val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java) } diff --git a/src/main/kotlin/com/restapi/controllers/Entities.kt b/src/main/kotlin/com/restapi/controllers/Entities.kt index 9807f7f..86f5c07 100644 --- a/src/main/kotlin/com/restapi/controllers/Entities.kt +++ b/src/main/kotlin/com/restapi/controllers/Entities.kt @@ -1,16 +1,23 @@ package com.restapi.controllers +import com.restapi.domain.ApprovalStatus import com.restapi.domain.DataModel +import com.restapi.domain.EntityModel import com.restapi.domain.Session import com.restapi.domain.Session.database import com.restapi.domain.Session.findByEntityAndId +import com.restapi.integ.Scripting import io.ebean.CallableSql import io.ebean.RawSqlBuilder +import io.javalin.http.BadRequestResponse import io.javalin.http.Context import io.javalin.http.NotFoundResponse import io.javalin.http.bodyAsClass import org.slf4j.LoggerFactory +import java.time.LocalDate import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter data class PatchValue(val key: String, val value: Any) data class Query( @@ -46,6 +53,14 @@ object Entities { fun approve(ctx: Context) {} fun reject(ctx: Context) {} + fun executeScript(ctx: Context) { + val name = ctx.pathParam("name") + val params = ctx.bodyAsClass>() + ctx.json( + Scripting.execute(name, params) + ) + } + fun executeStoredProcedure(ctx: Context) { val name = ctx.pathParam("name") val params = ctx.bodyAsClass>() @@ -58,7 +73,12 @@ object Entities { cs.setParameter(index + 1, entry.value) } cs.setParameter(params.entries.size + 1, Session.currentTenant()) - database.execute(cs) + val done = database.execute(cs) + ctx.json( + mapOf( + "done" to done + ) + ) } fun sqlQueryRaw(ctx: Context) { @@ -77,7 +97,7 @@ object Entities { } - fun sqlQueryId(ctx: Context) { + fun sqlQueryById(ctx: Context) { val sql = ctx.bodyAsClass() val query = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id")) @@ -104,16 +124,112 @@ object Entities { } fun create(ctx: Context) { + val entity = ctx.pathParam("entity") - val seqCreated = Session.creatSeq(entity) - logger.debug("sequence created for $entity? = $seqCreated") - database.save( - ctx.bodyAsClass().apply { - this.entityName = entity - if (this.uniqueIdentifier.isEmpty()) { - this.uniqueIdentifier = Session.nextUniqId(entity) - } + + + //may be approval flow is configured? + val setupEntity = database.find(EntityModel::class.java) + .where() + .eq("name", entity) + .findOne() + + Session.creatSeq(entity) + val dataModel = ctx.bodyAsClass().apply { + this.entityName = entity + if (this.uniqueIdentifier.isEmpty()) { + this.uniqueIdentifier = Session.nextUniqId(entity) } - ) + } + + database.save( + dataModel.apply { + if (setupEntity != null) { + + val allowedFields = setupEntity.allowedFields.map { it.lowercase() } + if (allowedFields.isNotEmpty()) { + + val moreFields = + dataModel.data.keys.map { it.lowercase() }.filter { !allowedFields.contains(it) } + + if (moreFields.isNotEmpty()) { + logger.warn("Data Keys = ${dataModel.data.keys} is more than $allowedFields, extra fields = $moreFields") + throw BadRequestResponse("data contains more fields than allowed") + } + + setupEntity.allowedFieldTypes.forEach { (key, expectedType) -> + + val valueFromUser = dataModel.data[key] ?: return@forEach + val isDate = expectedType.equals("date", ignoreCase = true) + val isDateTime = expectedType.equals("datetime", ignoreCase = true) + val isTime = expectedType.equals("time", ignoreCase = true) + if (isDate || isDateTime || isTime) { + //this should be a string of a particular format + if (valueFromUser !is String) { + throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") + } else { + val dtPattern = Regex("^\\d{4}-\\d{2}-\\d{2}$") + val dtmPattern = Regex("^\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}$") + val timePattern = Regex("^\\d{2}:\\d{2}$") + + + if (isDate + && !dtPattern.matches(valueFromUser) + && !isValidDate(valueFromUser) + ) { + throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") + } + + + if (isDateTime + && !dtmPattern.matches(valueFromUser) + && !isValidDateTime(valueFromUser) + ) { + throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") + } + + if (isTime + && !timePattern.matches(valueFromUser) + && !isValidTime(valueFromUser) + ) { + throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") + } + } + } + if (valueFromUser.javaClass.simpleName != expectedType) { + throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") + } + } + } + + if (setupEntity.approvalLevels > 0) { + + this.approvalStatus = ApprovalStatus.PENDING + this.requiredApprovalLevels = setupEntity.approvalLevels + } + + } + }) + } + + private fun isValidDate(f: String) = try { + LocalDate.parse(f, DateTimeFormatter.ofPattern("yyyy-MM-dd")) + true + } catch (e: Exception) { + false + } + + private fun isValidDateTime(f: String) = try { + LocalDateTime.parse(f, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + true + } catch (e: Exception) { + false + } + + private fun isValidTime(f: String) = try { + LocalTime.parse(f, DateTimeFormatter.ofPattern("HH:mm")) + true + } catch (e: Exception) { + false } } \ No newline at end of file diff --git a/src/main/kotlin/com/restapi/domain/db.kt b/src/main/kotlin/com/restapi/domain/db.kt index 13b1084..50441d1 100644 --- a/src/main/kotlin/com/restapi/domain/db.kt +++ b/src/main/kotlin/com/restapi/domain/db.kt @@ -47,6 +47,7 @@ object Session { fun currentUser() = currentUser.get().userName fun currentTenant() = currentUser.get().tenant + fun currentRoles() = currentUser.get().roles fun Database.findByEntityAndId(entity: String, id: String): DataModel { return find(DataModel::class.java) diff --git a/src/main/kotlin/com/restapi/domain/models.kt b/src/main/kotlin/com/restapi/domain/models.kt index 46c764e..b86928c 100644 --- a/src/main/kotlin/com/restapi/domain/models.kt +++ b/src/main/kotlin/com/restapi/domain/models.kt @@ -20,6 +20,10 @@ import javax.persistence.* data class Comments(val text: String = "", val by: String = "", val at: LocalDateTime = LocalDateTime.now()) +enum class ApprovalStatus { + PENDING, APPROVED, REJECTED +} + @MappedSuperclass abstract class BaseModel : Model() { @Id @@ -51,6 +55,13 @@ abstract class BaseModel : Model() { var deletedBy: String? = null + var currentApprovalLevel: Int = 0 + + var requiredApprovalLevels: Int = 0 + + @Enumerated(EnumType.STRING) + var approvalStatus: ApprovalStatus = ApprovalStatus.PENDING + @DbArray var tags: MutableList = arrayListOf() @@ -70,11 +81,15 @@ open class TenantModel : BaseModel() { } enum class AuditType { - CREATE, UPDATE, DELETE, VIEW + CREATE, UPDATE, DELETE, VIEW, APPROVE, REJECT } +@Entity +@Index(columnNames = ["audit_type", "entity", "unique_identifier", "tenant_id", "created_by"]) open class AuditLog : BaseModel() { + @Enumerated(EnumType.STRING) var auditType: AuditType = AuditType.CREATE + var entity: String = "" var uniqueIdentifier: String = "" @@ -108,6 +123,10 @@ open class EntityModel : BaseModel() { @DbArray var allowedFields: List = emptyList() + //enforce field types, if this is present, only fields that are present is validated + @DbJsonB + var allowedFieldTypes: Map = hashMapOf() + //when an entity is saved/updated audit logs will be populated, when this is empty, all fields are logged @DbArray var auditLogFields: List = emptyList() @@ -116,7 +135,7 @@ open class EntityModel : BaseModel() { var preferences: MutableMap = 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 + //a user needs to have ROLE_ENTITY_APPROVE_LEVEL1, ROLE_ENTITY_APPROVE_LEVEL2 roles for further approvals var approvalLevels: Int = 0 } diff --git a/src/main/kotlin/com/restapi/integ/Scripting.kt b/src/main/kotlin/com/restapi/integ/Scripting.kt index 6a6f991..ed0cb0a 100644 --- a/src/main/kotlin/com/restapi/integ/Scripting.kt +++ b/src/main/kotlin/com/restapi/integ/Scripting.kt @@ -1,43 +1,17 @@ package com.restapi.integ +import com.restapi.config.AppConfig.Companion.appConfig +import java.io.File import javax.script.Invocable import javax.script.ScriptEngineManager object Scripting { - const val a = "1" - @JvmStatic - fun main(args: Array) { - - - k() - - - } - - fun k(){ + fun execute(name: String, params: Map): Any { val engine = ScriptEngineManager().getEngineByExtension("kts")!! - val res1 = engine.eval(""" - fun fn(x: Int) = x + 2 - val obj = object { - fun fn1(x: Int) = x + 3 - } - obj""".trimIndent()) - println(res1) - - val invocator = engine as? Invocable - println(invocator) - - try { - println(invocator!!.invokeFunction("fn1", 3)) - } catch (e: NoSuchMethodException) { - println(e) - } - - val res2 = invocator!!.invokeFunction("fn", 3) - println(res2) - val res3 = invocator.invokeMethod(res1, "fn1", 3) - println(res3) + engine.eval(File(appConfig.scriptsPath(), "$name.kts").reader()) + val invocator = engine as Invocable + return invocator.invokeFunction("execute", params) } } \ No newline at end of file diff --git a/src/main/resources/dbmigration/1.0__initial.sql b/src/main/resources/dbmigration/1.0__initial.sql index 9042863..e43668f 100644 --- a/src/main/resources/dbmigration/1.0__initial.sql +++ b/src/main/resources/dbmigration/1.0__initial.sql @@ -1,13 +1,42 @@ -- apply changes -create table data_model ( +create table audit_log ( sys_pk bigint generated by default as identity not null, deleted_on timestamp, + current_approval_level integer not null, + required_approval_levels integer not null, deleted boolean default false not null, version integer not null, created_at timestamp not null, modified_at timestamp not null, tenant_id varchar(255) not null, deleted_by varchar(255), + approval_status varchar(8) not null, + tags varchar[] not null, + comments jsonb not null, + audit_type varchar(7) not null, + entity varchar(255) not null, + unique_identifier varchar(255) not null, + data jsonb not null, + changes jsonb not null, + created_by varchar(255) not null, + modified_by varchar(255) not null, + constraint ck_audit_log_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')), + constraint ck_audit_log_audit_type check ( audit_type in ('CREATE','UPDATE','DELETE','VIEW','APPROVE','REJECT')), + constraint pk_audit_log primary key (sys_pk) +); + +create table data_model ( + sys_pk bigint generated by default as identity not null, + deleted_on timestamp, + current_approval_level integer not null, + required_approval_levels integer not null, + deleted boolean default false not null, + version integer not null, + created_at timestamp not null, + modified_at timestamp not null, + tenant_id varchar(255) not null, + deleted_by varchar(255), + approval_status varchar(8) not null, tags varchar[] not null, comments jsonb not null, unique_identifier varchar(255) not null, @@ -15,9 +44,95 @@ create table data_model ( data jsonb not null, created_by varchar(255) not null, modified_by varchar(255) not null, + constraint ck_data_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')), constraint entity_unique_id unique (entity_name,unique_identifier,tenant_id), constraint pk_data_model primary key (sys_pk) ); +create table entity_model ( + sys_pk bigint generated by default as identity not null, + deleted_on timestamp, + current_approval_level integer not null, + required_approval_levels integer not null, + approval_levels integer not null, + deleted boolean default false not null, + version integer not null, + created_at timestamp not null, + modified_at timestamp not null, + tenant_id varchar(255) not null, + deleted_by varchar(255), + approval_status varchar(8) not null, + tags varchar[] not null, + comments jsonb not null, + name varchar(255) not null, + pre_save_script varchar(255) not null, + post_save_script varchar(255) not null, + actions varchar[] not null, + allowed_fields varchar[] not null, + allowed_field_types jsonb not null, + audit_log_fields varchar[] not null, + preferences jsonb not null, + created_by varchar(255) not null, + modified_by varchar(255) not null, + constraint ck_entity_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')), + constraint uq_entity_model_name unique (name), + constraint pk_entity_model primary key (sys_pk) +); + +create table job_model ( + sys_pk bigint generated by default as identity not null, + deleted_on timestamp, + current_approval_level integer not null, + required_approval_levels integer not null, + deleted boolean default false not null, + version integer not null, + created_at timestamp not null, + modified_at timestamp not null, + tenant_id varchar(255) not null, + deleted_by varchar(255), + approval_status varchar(8) not null, + tags varchar[] not null, + comments jsonb not null, + job_name varchar(255) not null, + job_type varchar(6) not null, + job_path varchar(255) not null, + tenants varchar[] not null, + job_frequency_type varchar(8) not null, + frequency varchar(255) not null, + created_by varchar(255) not null, + modified_by varchar(255) not null, + constraint ck_job_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')), + constraint ck_job_model_job_type check ( job_type in ('SCRIPT','DB')), + constraint ck_job_model_job_frequency_type check ( job_frequency_type in ('SPECIFIC','EVERY','CRON')), + constraint uq_job_model_job_name unique (job_name), + constraint pk_job_model primary key (sys_pk) +); + +create table tenant_model ( + sys_pk bigint generated by default as identity not null, + deleted_on timestamp, + current_approval_level integer not null, + required_approval_levels integer not null, + deleted boolean default false not null, + version integer not null, + created_at timestamp not null, + modified_at timestamp not null, + tenant_id varchar(255) not null, + deleted_by varchar(255), + approval_status varchar(8) not null, + tags varchar[] not null, + comments jsonb not null, + name varchar(255) not null, + domain varchar(255) not null, + preferences jsonb not null, + created_by varchar(255) not null, + modified_by varchar(255) not null, + constraint ck_tenant_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')), + constraint pk_tenant_model primary key (sys_pk) +); + -- foreign keys and indices +create index if not exists ix_audit_log_audit_type_entity_unique_identifier_tenant_i_1 on audit_log (audit_type,entity,unique_identifier,tenant_id,created_by); +create index audit_log_values_idx on audit_log using GIN (data) ; +create index audit_log_changes_idx on audit_log using GIN (changes) ; create index data_jsonb_idx on data_model using GIN (data) ; diff --git a/src/main/resources/dbmigration/model/1.0__initial.model.xml b/src/main/resources/dbmigration/model/1.0__initial.model.xml index 0d281e6..c92a04a 100644 --- a/src/main/resources/dbmigration/model/1.0__initial.model.xml +++ b/src/main/resources/dbmigration/model/1.0__initial.model.xml @@ -1,11 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -19,6 +44,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file