Merge remote-tracking branch 'refs/remotes/Simon/develop' into develop

This commit is contained in:
hongwei 2025-12-09 08:22:36 +01:00
commit 2ae58966c5
15 changed files with 595 additions and 20 deletions

View File

@ -355,8 +355,8 @@ object DynamicEndpointHelper extends RestHelper {
s"$roleNamePrefix$prettySummary${entitlementSuffix(path)}"
}
// substring role name to avoid it have over the maximum length of db column.
if(roleName.size > 64) {
roleName = StringUtils.substring(roleName, 0, 53) + roleName.hashCode()
if(roleName.size > 255) {
roleName = StringUtils.substring(roleName, 0, 244) + roleName.hashCode()
}
Some(List(
ApiRole.getOrCreateDynamicApiRole(roleName, bankId.isDefined)

View File

@ -693,6 +693,9 @@ object ApiRole extends MdcLoggable{
case class CanDeleteSystemLevelDynamicEntity(requiresBankId: Boolean = false) extends ApiRole
lazy val canDeleteSystemLevelDynamicEntity = CanDeleteSystemLevelDynamicEntity()
case class CanDeleteCascadeSystemDynamicEntity(requiresBankId: Boolean = false) extends ApiRole
lazy val canDeleteCascadeSystemDynamicEntity = CanDeleteCascadeSystemDynamicEntity()
case class CanDeleteBankLevelDynamicEntity(requiresBankId: Boolean = true) extends ApiRole
lazy val canDeleteBankLevelDynamicEntity = CanDeleteBankLevelDynamicEntity()

View File

@ -2538,6 +2538,14 @@ object ExampleValue {
| "type": "integer",
| "example": "698761728934",
| "description": "description of **number** field, can be markdown text."
| },
| "metadata": {
| "type": "json",
| "example": {
| "tags": ["important", "verified"],
| "settings": {"color": "blue", "priority": 1}
| },
| "description": "description of **metadata** field (JSON object or array), can be markdown text."
| }
| }
| }

View File

@ -2966,7 +2966,7 @@ object Glossary extends MdcLoggable {
|
|**Supported field types:**
|
|STRING, INTEGER, DOUBLE, BOOLEAN, DATE_WITH_DAY (format: yyyy-MM-dd), and reference types (foreign keys)
|STRING, INTEGER, DOUBLE, BOOLEAN, DATE_WITH_DAY (format: yyyy-MM-dd), JSON (objects and arrays), and reference types (foreign keys)
|
|**The hasPersonalEntity flag:**
|

View File

@ -106,6 +106,7 @@ object Migration extends MdcLoggable {
renameCustomerRoleNames()
addUniqueIndexOnResourceUserUserId()
addIndexOnMappedMetricUserId()
alterRoleNameLength()
}
private def dummyScript(): Boolean = {
@ -545,6 +546,13 @@ object Migration extends MdcLoggable {
MigrationOfUserIdIndexes.addIndexOnMappedMetricUserId(name)
}
}
private def alterRoleNameLength(): Boolean = {
val name = nameOf(alterRoleNameLength)
runOnce(name) {
MigrationOfRoleNameFieldLength.alterRoleNameLength(name)
}
}
}
/**

View File

@ -0,0 +1,80 @@
package code.api.util.migration
import code.api.util.APIUtil
import code.api.util.migration.Migration.{DbFunction, saveLog}
import code.entitlement.MappedEntitlement
import code.entitlementrequest.MappedEntitlementRequest
import code.scope.MappedScope
import net.liftweb.common.Full
import net.liftweb.mapper.Schemifier
import java.time.format.DateTimeFormatter
import java.time.{ZoneId, ZonedDateTime}
object MigrationOfRoleNameFieldLength {
val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1)
val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1)
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'")
def alterRoleNameLength(name: String): Boolean = {
val entitlementTableExists = DbFunction.tableExists(MappedEntitlement)
val entitlementRequestTableExists = DbFunction.tableExists(MappedEntitlementRequest)
val scopeTableExists = DbFunction.tableExists(MappedScope)
if (!entitlementTableExists || !entitlementRequestTableExists || !scopeTableExists) {
val startDate = System.currentTimeMillis()
val commitId: String = APIUtil.gitCommit
val isSuccessful = false
val endDate = System.currentTimeMillis()
val comment: String =
s"""One or more required tables do not exist:
|entitlement table exists: $entitlementTableExists
|entitlementrequest table exists: $entitlementRequestTableExists
|scope table exists: $scopeTableExists
|""".stripMargin
saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
return isSuccessful
}
val startDate = System.currentTimeMillis()
val commitId: String = APIUtil.gitCommit
var isSuccessful = false
val executedSql =
DbFunction.maybeWrite(true, Schemifier.infoF _) {
APIUtil.getPropsValue("db.driver") match {
case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") =>
() =>
"""
|ALTER TABLE mappedentitlement ALTER COLUMN mrolename varchar(255);
|ALTER TABLE mappedentitlementrequest ALTER COLUMN mrolename varchar(255);
|ALTER TABLE mappedscope ALTER COLUMN mrolename varchar(255);
|""".stripMargin
case _ =>
() =>
"""
|ALTER TABLE mappedentitlement ALTER COLUMN mrolename TYPE varchar(255);
|ALTER TABLE mappedentitlementrequest ALTER COLUMN mrolename TYPE varchar(255);
|ALTER TABLE mappedscope ALTER COLUMN mrolename TYPE varchar(255);
|""".stripMargin
}
}
val endDate = System.currentTimeMillis()
val comment: String =
s"""Executed SQL:
|$executedSql
|
|Increased mrolename column length from 64 to 255 characters in three tables:
| - mappedentitlement
| - mappedentitlementrequest
| - mappedscope
|
|This allows for longer dynamic entity names and role names.
|""".stripMargin
isSuccessful = true
saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
isSuccessful
}
}

View File

@ -14,7 +14,7 @@ import code.api.dynamic.endpoint.helper.practise.{
PractiseEndpoint
}
import code.api.dynamic.endpoint.helper.{CompiledObjects, DynamicEndpointHelper}
import code.api.dynamic.entity.helper.DynamicEntityInfo
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
import code.api.util.APIUtil.{fullBoxOrException, _}
import code.api.util.ApiRole._
import code.api.util.ApiTag._
@ -2321,6 +2321,17 @@ trait APIMethods400 extends MdcLoggable {
| "summary": {
| "type": "string",
| "example": "User received 'No such price' error using Stripe API"
| },
| "custom_metadata": {
| "type": "json",
| "example": {
| "priority": "high",
| "tags": ["support", "billing"],
| "context": {
| "page": "checkout",
| "step": 3
| }
| }
| }
| }
| },
@ -2513,6 +2524,17 @@ trait APIMethods400 extends MdcLoggable {
| "summary": {
| "type": "string",
| "example": "User received 'No such price' error using Stripe API"
| },
| "custom_metadata": {
| "type": "json",
| "example": {
| "priority": "high",
| "tags": ["support", "billing"],
| "context": {
| "page": "checkout",
| "step": 3
| }
| }
| }
| }
| },
@ -2801,6 +2823,8 @@ trait APIMethods400 extends MdcLoggable {
}
}
private def deleteDynamicEntityMethod(
bankId: Option[String],
dynamicEntityId: String,

View File

@ -9,8 +9,9 @@ import code.api.util.APIUtil._
import code.api.util.ApiRole
import code.api.util.ApiRole._
import code.api.util.ApiTag._
import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidDateFormat, InvalidJsonFormat, UnknownError, _}
import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidDateFormat, InvalidJsonFormat, UnknownError, DynamicEntityOperationNotAllowed, _}
import code.api.util.FutureUtil.EndpointContext
import code.api.util.Glossary
import code.api.util.NewStyle.HttpCode
import code.api.util.{APIUtil, CallContext, DiagnosticDynamicEntityCheck, ErrorMessages, NewStyle, RateLimitingUtil}
import code.api.util.NewStyle.function.extractQueryParams
@ -24,6 +25,7 @@ import code.api.v4_0_0.JSONFactory400.createCallsLimitJson
import code.api.v5_0_0.JSONFactory500
import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
import code.api.v6_0_0.OBPAPI6_0_0
import code.metrics.APIMetrics
@ -39,16 +41,23 @@ import code.util.Helper.{MdcLoggable, ObpS, SILENCE_IS_GOLDEN}
import code.views.Views
import code.views.system.ViewDefinition
import code.webuiprops.{MappedWebUiPropsProvider, WebUiPropsCommons}
import code.dynamicEntity.DynamicEntityCommons
import code.DynamicData.{DynamicData, DynamicDataProvider}
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model.{CustomerAttribute, _}
import com.openbankproject.commons.model.enums.DynamicEntityOperation._
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import net.liftweb.common.{Empty, Full}
import net.liftweb.common.{Empty, Failure, Full}
import org.apache.commons.lang3.StringUtils
import net.liftweb.http.provider.HTTPParam
import net.liftweb.http.rest.RestHelper
import net.liftweb.json.{Extraction, JsonParser}
import net.liftweb.json.JsonAST.JValue
import net.liftweb.mapper.{By, Descending, MaxRows, OrderBy}
import net.liftweb.json.JsonAST.{JArray, JObject, JString, JValue}
import net.liftweb.json.JsonDSL._
import net.liftweb.mapper.{By, Descending, MaxRows, NullRef, OrderBy}
import code.api.util.ExampleValue.dynamicEntityResponseBodyExample
import net.liftweb.common.Box
import java.text.SimpleDateFormat
import java.util.UUID.randomUUID
@ -2472,7 +2481,7 @@ trait APIMethods600 {
Constant.HostName + "/" + code.model.dataAccess.AuthUser.validateUserPath.mkString("/") + "/" + java.net.URLEncoder.encode(savedUser.uniqueId.get, "UTF-8")
case _ =>
// Default to portal_external_url property if available, otherwise fall back to hostname
APIUtil.getPropsValue("portal_external_url", Constant.HostName) + "/" + code.model.dataAccess.AuthUser.validateUserPath.mkString("/") + "/" + java.net.URLEncoder.encode(savedUser.uniqueId.get, "UTF-8")
APIUtil.getPropsValue("portal_external_url", Constant.HostName) + "/user-validation?token=" + java.net.URLEncoder.encode(savedUser.uniqueId.get, "UTF-8")
}
val textContent = Some(s"Welcome! Please validate your account by clicking the following link: $emailValidationLink")
@ -3420,6 +3429,269 @@ trait APIMethods600 {
}
}
staticResourceDocs += ResourceDoc(
getWebUiProps,
implementedInApiVersion,
nameOf(getWebUiProps),
"GET",
"/management/webui_props",
"Get WebUiProps",
s"""
|
|Get WebUiProps - properties that configure the Web UI behavior and appearance.
|
|Properties with names starting with "webui_" can be stored in the database and managed via API.
|
|**Data Sources:**
|
|1. **Explicit WebUiProps (Database)**: Custom values created/updated via the API and stored in the database.
|
|2. **Implicit WebUiProps (Configuration File)**: Default values defined in the `sample.props.template` configuration file.
|
|**Query Parameter:**
|
|* `what` (optional, string, default: "active")
| - `active`: Returns explicit props from database + implicit (default) props from configuration file
| - When both sources have the same property name, the database value takes precedence
| - Implicit props are marked with `webUiPropsId = "default"`
| - `database`: Returns only explicit props from the database
| - `config`: Returns only implicit (default) props from configuration file
|
|**Examples:**
|
|Get database props combined with defaults (default behavior):
|${getObpApiRoot}/v6.0.0/management/webui_props
|${getObpApiRoot}/v6.0.0/management/webui_props?what=active
|
|Get only database-stored props:
|${getObpApiRoot}/v6.0.0/management/webui_props?what=database
|
|Get only default props from configuration:
|${getObpApiRoot}/v6.0.0/management/webui_props?what=config
|
|For more details about WebUI Props, including how to set config file defaults and precedence order, see ${Glossary.getGlossaryItemLink("webui_props")}.
|
|""",
EmptyBody,
ListResult(
"webui_props",
(List(WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com", Some("web-ui-props-id"))))
)
,
List(
UserNotLoggedIn,
UserHasMissingRoles,
UnknownError
),
List(apiTagWebUiProps),
Some(List(canGetWebUiProps))
)
lazy val getWebUiProps: OBPEndpoint = {
case "management" :: "webui_props":: Nil JsonGet req => {
cc => implicit val ec = EndpointContext(Some(cc))
val what = ObpS.param("what").getOrElse("active")
logger.info(s"========== GET /obp/v6.0.0/management/webui_props called with what=$what ==========")
for {
(Full(u), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.tryons(s"""$InvalidFilterParameterFormat `what` must be one of: active, database, config. Current value: $what""", 400, callContext) {
what match {
case "active" | "database" | "config" => true
case _ => false
}
}
_ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetWebUiProps, callContext)
explicitWebUiProps <- Future{ MappedWebUiPropsProvider.getAll() }
implicitWebUiProps = getWebUIPropsPairs.map(webUIPropsPairs=>WebUiPropsCommons(webUIPropsPairs._1, webUIPropsPairs._2, webUiPropsId= Some("default")))
result = what match {
case "database" =>
// Return only database props
explicitWebUiProps
case "config" =>
// Return only config file props
implicitWebUiProps.distinct
case "active" =>
// Return database props + config props (removing duplicates, database takes precedence)
val implicitWebUiPropsRemovedDuplicated = if(explicitWebUiProps.nonEmpty){
val duplicatedProps : List[WebUiPropsCommons]= explicitWebUiProps.map(explicitWebUiProp => implicitWebUiProps.filter(_.name == explicitWebUiProp.name)).flatten
implicitWebUiProps diff duplicatedProps
} else {
implicitWebUiProps.distinct
}
explicitWebUiProps ++ implicitWebUiPropsRemovedDuplicated
}
} yield {
logger.info(s"========== GET /obp/v6.0.0/management/webui_props returning ${result.size} records ==========")
result.foreach { prop =>
logger.info(s" - name: ${prop.name}, value: ${prop.value}, webUiPropsId: ${prop.webUiPropsId}")
}
logger.info(s"========== END GET /obp/v6.0.0/management/webui_props ==========")
(ListResult("webui_props", result), HttpCode.`200`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
getSystemDynamicEntities,
implementedInApiVersion,
nameOf(getSystemDynamicEntities),
"GET",
"/management/system-dynamic-entities",
"Get System Dynamic Entities",
s"""Get all System Dynamic Entities with record counts.
|
|Each dynamic entity in the response includes a `record_count` field showing how many data records exist for that entity.
|
|For more information see ${Glossary.getGlossaryItemLink(
"Dynamic-Entities"
)} """,
EmptyBody,
ListResult(
"dynamic_entities",
List(dynamicEntityResponseBodyExample)
),
List(
$UserNotLoggedIn,
UserHasMissingRoles,
UnknownError
),
List(apiTagManageDynamicEntity, apiTagApi),
Some(List(canGetSystemLevelDynamicEntities))
)
lazy val getSystemDynamicEntities: OBPEndpoint = {
case "management" :: "system-dynamic-entities" :: Nil JsonGet req => {
cc =>
implicit val ec = EndpointContext(Some(cc))
for {
dynamicEntities <- Future(
NewStyle.function.getDynamicEntities(None, false)
)
} yield {
val listCommons: List[DynamicEntityCommons] = dynamicEntities.sortBy(_.entityName)
val jObjectsWithCounts = listCommons.map { entity =>
val recordCount = DynamicData.count(
By(DynamicData.DynamicEntityName, entity.entityName),
By(DynamicData.IsPersonalEntity, false),
if (entity.bankId.isEmpty) NullRef(DynamicData.BankId) else By(DynamicData.BankId, entity.bankId.get)
)
entity.jValue.asInstanceOf[JObject] ~ ("record_count" -> recordCount)
}
(
ListResult("dynamic_entities", jObjectsWithCounts),
HttpCode.`200`(cc.callContext)
)
}
}
}
private def unboxResult[T: Manifest](box: Box[T], entityName: String): T = {
if (box.isInstanceOf[Failure]) {
val failure = box.asInstanceOf[Failure]
// change the internal db column name 'dynamicdataid' to entity's id name
val msg = failure.msg.replace(
DynamicData.DynamicDataId.dbColumnName,
StringUtils.uncapitalize(entityName) + "Id"
)
val changedMsgFailure = failure.copy(msg = s"$InternalServerError $msg")
fullBoxOrException[T](changedMsgFailure)
}
box.openOrThrowException(s"$UnknownError ")
}
staticResourceDocs += ResourceDoc(
deleteSystemDynamicEntityCascade,
implementedInApiVersion,
nameOf(deleteSystemDynamicEntityCascade),
"DELETE",
"/management/system-dynamic-entities/cascade/DYNAMIC_ENTITY_ID",
"Delete System Level Dynamic Entity Cascade",
s"""Delete a DynamicEntity specified by DYNAMIC_ENTITY_ID and all its data records.
|
|This endpoint performs a cascade delete:
|1. Deletes all data records associated with the dynamic entity
|2. Deletes the dynamic entity definition itself
|
|Use with caution - this operation cannot be undone.
|
|For more information see ${Glossary.getGlossaryItemLink(
"Dynamic-Entities"
)}/
|
|""",
EmptyBody,
EmptyBody,
List(
$UserNotLoggedIn,
UserHasMissingRoles,
UnknownError
),
List(apiTagManageDynamicEntity, apiTagApi),
Some(List(canDeleteCascadeSystemDynamicEntity))
)
lazy val deleteSystemDynamicEntityCascade: OBPEndpoint = {
case "management" :: "system-dynamic-entities" :: "cascade" :: dynamicEntityId :: Nil JsonDelete _ => {
cc =>
implicit val ec = EndpointContext(Some(cc))
deleteDynamicEntityCascadeMethod(None, dynamicEntityId, cc)
}
}
private def deleteDynamicEntityCascadeMethod(
bankId: Option[String],
dynamicEntityId: String,
cc: CallContext
) = {
for {
// Get the dynamic entity
(entity, _) <- NewStyle.function.getDynamicEntityById(
bankId,
dynamicEntityId,
cc.callContext
)
// Get all data records for this entity
(box, _) <- NewStyle.function.invokeDynamicConnector(
GET_ALL,
entity.entityName,
None,
None,
entity.bankId,
None,
None,
false,
cc.callContext
)
resultList: JArray = unboxResult(
box.asInstanceOf[Box[JArray]],
entity.entityName
)
// Delete all data records
_ <- Future.sequence {
resultList.arr.map { record =>
val idFieldName = DynamicEntityHelper.createEntityId(entity.entityName)
val recordId = (record \ idFieldName).asInstanceOf[JString].s
Future {
DynamicDataProvider.connectorMethodProvider.vend.delete(
entity.bankId,
entity.entityName,
recordId,
None,
false
)
}
}
}
// Delete the dynamic entity definition
deleted: Box[Boolean] <- NewStyle.function.deleteDynamicEntity(
bankId,
dynamicEntityId
)
} yield {
(deleted, HttpCode.`200`(cc.callContext))
}
}
}
}

View File

@ -4,6 +4,7 @@ import java.util.regex.Pattern
import code.api.util.ErrorMessages.DynamicEntityInstanceValidateFail
import code.api.util.{APIUtil, CallContext, NewStyle}
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model.enums.{DynamicEntityFieldType, DynamicEntityOperation}
import com.openbankproject.commons.model._
@ -144,7 +145,7 @@ trait DynamicEntityT {
}
}
object ReferenceType {
object ReferenceType extends MdcLoggable {
private def recoverFn(fieldName: String, value: String, entityName: String): PartialFunction[Throwable, String] = {
case _: Throwable => s"entity '$entityName' not found by the value '$value', the field name is '$fieldName'."
@ -360,14 +361,18 @@ object ReferenceType {
} else {
val dynamicEntityName = typeName.replace("reference:", "")
val errorMsg = s"""$dynamicEntityName not found by the id value '$value', propertyName is '$propertyName'"""
NewStyle.function.invokeDynamicConnector(DynamicEntityOperation.GET_ONE,dynamicEntityName, None, Some(value), None, None, None, false,callContext)
.recover {
case _: Throwable => errorMsg
}
.map {
case (Full(_), _) => ""
case _ => errorMsg
logger.info(s"========== Validating reference field: propertyName='$propertyName', typeName='$typeName', dynamicEntityName='$dynamicEntityName', value='$value' ==========")
Future {
val exists = code.DynamicData.MappedDynamicDataProvider.existsById(dynamicEntityName, value)
if (exists) {
logger.info(s"========== Reference validation SUCCESS: propertyName='$propertyName', dynamicEntityName='$dynamicEntityName', value='$value' ==========")
""
} else {
logger.warn(s"========== Reference validation FAILED: propertyName='$propertyName', dynamicEntityName='$dynamicEntityName', value='$value' ==========")
errorMsg
}
}
}
}
}

View File

@ -25,6 +25,17 @@ object MappedDynamicDataProvider extends DynamicDataProvider with CustomJsonForm
saveOrUpdate(bankId, entityName, requestBody, userId, isPersonalEntity, dynamicData)
}
// Separate method for reference validation - only checks ID and entity name exist
def existsById(entityName: String, id: String): Boolean = {
println(s"========== Reference validation: checking if DynamicDataId='$id' exists for DynamicEntityName='$entityName' ==========")
val exists = DynamicData.count(
By(DynamicData.DynamicDataId, id),
By(DynamicData.DynamicEntityName, entityName)
) > 0
println(s"========== Reference validation result: exists=$exists ==========")
exists
}
override def get(bankId: Option[String],entityName: String, id: String, userId: Option[String], isPersonalEntity: Boolean): Box[DynamicDataT] = {
if(bankId.isEmpty && !isPersonalEntity ){ //isPersonalEntity == false, get all the data, no need for specific userId.
//forced the empty also to a error here. this is get Dynamic by Id, if it return Empty, better show the error in this level.

View File

@ -139,7 +139,7 @@ class MappedEntitlement extends Entitlement
object mEntitlementId extends MappedUUID(this)
object mBankId extends UUIDString(this)
object mUserId extends UUIDString(this)
object mRoleName extends MappedString(this, 64)
object mRoleName extends MappedString(this, 255)
object mCreatedByProcess extends MappedString(this, 255)
object mGroupId extends MappedString(this, 255) {

View File

@ -87,7 +87,7 @@ class MappedEntitlementRequest extends EntitlementRequest
object mUserId extends UUIDString(this)
object mRoleName extends MappedString(this, 64)
object mRoleName extends MappedString(this, 255)
override def entitlementRequestId: String = mEntitlementRequestId.get.toString

View File

@ -85,7 +85,7 @@ class MappedScope extends Scope
object mScopeId extends MappedUUID(this)
object mBankId extends UUIDString(this)
object mConsumerId extends UUIDString(this)
object mRoleName extends MappedString(this, 64)
object mRoleName extends MappedString(this, 255)
override def scopeId: String = mScopeId.get.toString
override def bankId: String = mBankId.get

View File

@ -0,0 +1,153 @@
/**
Open Bank Project - API
Copyright (C) 2011-2019, TESOBE GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Email: contact@tesobe.com
TESOBE GmbH
Osloerstrasse 16/17
Berlin 13359, Germany
This product includes software developed at
TESOBE (http://www.tesobe.com/)
*/
package code.api.v6_0_0
import code.api.util.APIUtil.OAuth._
import code.api.util.ApiRole._
import code.api.util.ErrorMessages._
import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0
import code.entitlement.Entitlement
import code.webuiprops.WebUiPropsCommons
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.ApiVersion
import net.liftweb.json.Serialization.write
import org.scalatest.Tag
class WebUiPropsTest extends V600ServerSetup {
/**
* Test tags
* Example: To run tests with tag "getPermissions":
* mvn test -D tagsToInclude
*
* This is made possible by the scalatest maven plugin
*/
object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString)
object ApiEndpoint extends Tag(nameOf(Implementations6_0_0.getWebUiProp))
val rightEntity = WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com")
val anotherEntity = WebUiPropsCommons("webui_api_manager_url", "https://apimanager.openbankproject.com")
val wrongEntity = WebUiPropsCommons("hello_api_explorer_url", "https://apiexplorer.openbankproject.com") // name not start with "webui_"
feature("Get Single WebUiProp by Name v6.0.0") {
scenario("Get WebUiProp - successful case with explicit prop from database", VersionOfApi, ApiEndpoint) {
// First create a webui prop
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString)
When("We create a webui prop")
val requestCreate = (v6_0_0_Request / "management" / "webui_props").POST <@(user1)
val responseCreate = makePostRequest(requestCreate, write(rightEntity))
Then("We should get a 201")
responseCreate.code should equal(201)
When("We get the webui prop by name without active flag")
val requestGet = (v6_0_0_Request / "webui-props" / rightEntity.name).GET
val responseGet = makeGetRequest(requestGet)
Then("We should get a 200")
responseGet.code should equal(200)
val webUiPropJson = responseGet.body.extract[WebUiPropsCommons]
webUiPropJson.name should equal(rightEntity.name)
webUiPropJson.value should equal(rightEntity.value)
}
scenario("Get WebUiProp - successful case with active=true returns explicit prop", VersionOfApi, ApiEndpoint) {
// First create a webui prop
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString)
When("We create a webui prop")
val requestCreate = (v6_0_0_Request / "management" / "webui_props").POST <@(user1)
val responseCreate = makePostRequest(requestCreate, write(anotherEntity))
Then("We should get a 201")
responseCreate.code should equal(201)
When("We get the webui prop by name with active=true")
val requestGet = (v6_0_0_Request / "webui-props" / anotherEntity.name).GET.addQueryParameter("active", "true")
val responseGet = makeGetRequest(requestGet)
Then("We should get a 200")
responseGet.code should equal(200)
val webUiPropJson = responseGet.body.extract[WebUiPropsCommons]
webUiPropJson.name should equal(anotherEntity.name)
webUiPropJson.value should equal(anotherEntity.value)
}
scenario("Get WebUiProp - not found without active flag", VersionOfApi, ApiEndpoint) {
When("We get a non-existent webui prop by name without active flag")
val requestGet = (v6_0_0_Request / "webui-props" / "webui_non_existent_prop").GET
val responseGet = makeGetRequest(requestGet)
Then("We should get a 400")
responseGet.code should equal(400)
val error = responseGet.body.extract[ErrorMessage]
error.message should include(WebUiPropsNotFoundByName)
}
scenario("Get WebUiProp - with active=true returns implicit prop from config", VersionOfApi, ApiEndpoint) {
// Test that we can get implicit props from sample.props.template when active=true
When("We get a webui prop by name with active=true that exists in config but not in DB")
// Use a prop that should exist in sample.props.template like webui_sandbox_introduction
val requestGet = (v6_0_0_Request / "webui-props" / "webui_sandbox_introduction").GET.addQueryParameter("active", "true")
val responseGet = makeGetRequest(requestGet)
Then("We should get a 200 with implicit prop")
responseGet.code should equal(200)
val webUiPropJson = responseGet.body.extract[WebUiPropsCommons]
webUiPropJson.name should equal("webui_sandbox_introduction")
webUiPropJson.webUiPropsId should equal(Some("default"))
}
scenario("Get WebUiProp - invalid active parameter", VersionOfApi, ApiEndpoint) {
When("We get a webui prop with invalid active parameter")
val requestGet = (v6_0_0_Request / "webui-props" / "webui_api_explorer_url").GET.addQueryParameter("active", "invalid")
val responseGet = makeGetRequest(requestGet)
Then("We should get a 400")
responseGet.code should equal(400)
val error = responseGet.body.extract[ErrorMessage]
error.message should include(InvalidFilterParameterFormat)
}
scenario("Get WebUiProp - database prop takes precedence over config prop when active=true", VersionOfApi, ApiEndpoint) {
// Create a webui prop that overrides a config value
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString)
val customValue = WebUiPropsCommons("webui_get_started_text", "Custom Get Started Text")
When("We create a webui prop that overrides config")
val requestCreate = (v6_0_0_Request / "management" / "webui_props").POST <@(user1)
val responseCreate = makePostRequest(requestCreate, write(customValue))
Then("We should get a 201")
responseCreate.code should equal(201)
When("We get the webui prop with active=true")
val requestGet = (v6_0_0_Request / "webui-props" / customValue.name).GET.addQueryParameter("active", "true")
val responseGet = makeGetRequest(requestGet)
Then("We should get the database value, not the config value")
responseGet.code should equal(200)
val webUiPropJson = responseGet.body.extract[WebUiPropsCommons]
webUiPropJson.name should equal(customValue.name)
webUiPropJson.value should equal(customValue.value)
webUiPropJson.webUiPropsId should not equal(Some("default"))
}
}
}

View File

@ -249,6 +249,17 @@ object DynamicEntityFieldType extends OBPEnumeration[DynamicEntityFieldType]{
override def wrongTypeMsg: String = s"the value's type should be $this, format is $dateFormat."
}
object json extends Value {
val jValueType = classOf[JValue]
override def isJValueValid(jValue: JValue): Boolean = {
jValue match {
case _: JObject => true
case _: JArray => true
case _ => false
}
}
override def wrongTypeMsg: String = "the value's type should be a JSON object or array."
}
//object array extends Value{val jValueType = classOf[JArray]}
//object `object` extends Value{val jValueType = classOf[JObject]} //TODO in the future, we consider support nested type
}