diff --git a/obp-api/pom.xml b/obp-api/pom.xml
index 0e4fead345..3bb1e89ccf 100644
--- a/obp-api/pom.xml
+++ b/obp-api/pom.xml
@@ -225,7 +225,7 @@
org.apache.pekko
pekko-http-core_${scala.version}
- 1.1.0
+ ${pekko-http.version}
org.apache.pekko
@@ -304,6 +304,12 @@
oauth2-oidc-sdk
11.37.1
+
+
+ net.minidev
+ json-smart
+
diff --git a/obp-api/src/main/scala/code/api/constant/constant.scala b/obp-api/src/main/scala/code/api/constant/constant.scala
index 7cdcd86276..17e62f40dc 100644
--- a/obp-api/src/main/scala/code/api/constant/constant.scala
+++ b/obp-api/src/main/scala/code/api/constant/constant.scala
@@ -586,6 +586,78 @@ object Constant extends MdcLoggable {
CAN_ANSWER_TRANSACTION_REQUEST_CHALLENGE
)
+ // Auditor system view: read-only on the account itself. The auditor can
+ // see everything but cannot modify account data, counterparties, images,
+ // locations, aliases, URLs, or initiate / approve payments.
+ // The only writes allowed are the auditor's own annotations: comments and
+ // tags (including where_tags). Those are scoped to the auditor's view as
+ // separate metadata — they do not change the account or its data.
+ final val SYSTEM_AUDITOR_VIEW_PERMISSION = List(
+ // See transactions
+ CAN_SEE_TRANSACTION_THIS_BANK_ACCOUNT,
+ CAN_SEE_TRANSACTION_OTHER_BANK_ACCOUNT,
+ CAN_SEE_TRANSACTION_METADATA,
+ CAN_SEE_TRANSACTION_AMOUNT,
+ CAN_SEE_TRANSACTION_TYPE,
+ CAN_SEE_TRANSACTION_CURRENCY,
+ CAN_SEE_TRANSACTION_START_DATE,
+ CAN_SEE_TRANSACTION_FINISH_DATE,
+ CAN_SEE_TRANSACTION_BALANCE,
+ CAN_SEE_TRANSACTION_STATUS,
+ // See this account
+ CAN_SEE_BANK_ACCOUNT_OWNERS,
+ CAN_SEE_BANK_ACCOUNT_TYPE,
+ CAN_SEE_BANK_ACCOUNT_BALANCE,
+ CAN_SEE_BANK_ACCOUNT_CURRENCY,
+ CAN_SEE_BANK_ACCOUNT_LABEL,
+ CAN_SEE_BANK_ACCOUNT_NATIONAL_IDENTIFIER,
+ CAN_SEE_BANK_ACCOUNT_SWIFT_BIC,
+ CAN_SEE_BANK_ACCOUNT_IBAN,
+ CAN_SEE_BANK_ACCOUNT_NUMBER,
+ CAN_SEE_BANK_ACCOUNT_BANK_NAME,
+ CAN_SEE_BANK_ACCOUNT_BANK_PERMALINK,
+ CAN_SEE_BANK_ACCOUNT_ROUTING_SCHEME,
+ CAN_SEE_BANK_ACCOUNT_ROUTING_ADDRESS,
+ // See bank
+ CAN_SEE_BANK_ROUTING_SCHEME,
+ CAN_SEE_BANK_ROUTING_ADDRESS,
+ // See other accounts
+ CAN_SEE_OTHER_ACCOUNT_NATIONAL_IDENTIFIER,
+ CAN_SEE_OTHER_ACCOUNT_SWIFT_BIC,
+ CAN_SEE_OTHER_ACCOUNT_IBAN,
+ CAN_SEE_OTHER_ACCOUNT_BANK_NAME,
+ CAN_SEE_OTHER_ACCOUNT_NUMBER,
+ CAN_SEE_OTHER_ACCOUNT_METADATA,
+ CAN_SEE_OTHER_ACCOUNT_KIND,
+ CAN_SEE_OTHER_ACCOUNT_ROUTING_SCHEME,
+ CAN_SEE_OTHER_ACCOUNT_ROUTING_ADDRESS,
+ CAN_SEE_OTHER_BANK_ROUTING_SCHEME,
+ CAN_SEE_OTHER_BANK_ROUTING_ADDRESS,
+ // See metadata (everyone's)
+ CAN_SEE_COMMENTS,
+ CAN_SEE_OWNER_COMMENT,
+ CAN_SEE_TAGS,
+ CAN_SEE_WHERE_TAG,
+ CAN_SEE_IMAGES,
+ CAN_SEE_MORE_INFO,
+ CAN_SEE_URL,
+ CAN_SEE_IMAGE_URL,
+ CAN_SEE_OPEN_CORPORATES_URL,
+ CAN_SEE_CORPORATE_LOCATION,
+ CAN_SEE_PHYSICAL_LOCATION,
+ CAN_SEE_PUBLIC_ALIAS,
+ CAN_SEE_PRIVATE_ALIAS,
+ // Read counterparties (no modification)
+ CAN_GET_COUNTERPARTY,
+ // Auditor's own annotations only
+ CAN_ADD_COMMENT,
+ CAN_DELETE_COMMENT,
+ CAN_ADD_TAG,
+ CAN_DELETE_TAG,
+ CAN_ADD_WHERE_TAG,
+ CAN_DELETE_WHERE_TAG
+ )
+
final val ALL_VIEW_PERMISSION_NAMES = List(
CAN_SEE_TRANSACTION_OTHER_BANK_ACCOUNT,
CAN_SEE_TRANSACTION_METADATA,
diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala
index 9125dcab9a..2c6cd62154 100644
--- a/obp-api/src/main/scala/code/api/util/Glossary.scala
+++ b/obp-api/src/main/scala/code/api/util/Glossary.scala
@@ -939,6 +939,21 @@ object Glossary extends MdcLoggable {
|
|
|Example value: ${bankIdExample.value}
+ |
+ |## Version history
+ |
+ |The JSON field name for this identifier changed across OBP-API versions:
+ |
+ |- **v6.0.0+** (current): `bank_id` — the canonical field name in both request and response bodies (e.g. `PostBankJson600`, `BankJson600`).
+ |- **v5.0.0**: `id` (Option[String]) — see `PostBankJson500` / `BankJson500`.
+ |- **v4.0.0**: `id` (String), plus a now-removed `short_name` field — see `PostBankJson400` / `BankJson400`.
+ |
+ |The v6 createBank request body shape is exactly:
+ |`bank_id`, `bank_code`, `full_name`, `logo`, `website`, `bank_routings`.
+ |
+ |If you're regenerating client code from older docs, samples, or LLM training data, double-check
+ |the field name — sending `id` to v6 endpoints will silently produce an empty `bank_id` and
+ |fail validation with a confusing length error.
""")
glossaryItems += GlossaryItem(
diff --git a/obp-api/src/main/scala/code/api/util/newstyle/ViewNewStyle.scala b/obp-api/src/main/scala/code/api/util/newstyle/ViewNewStyle.scala
index 4156e01926..49375e80ed 100644
--- a/obp-api/src/main/scala/code/api/util/newstyle/ViewNewStyle.scala
+++ b/obp-api/src/main/scala/code/api/util/newstyle/ViewNewStyle.scala
@@ -103,6 +103,12 @@ object ViewNewStyle {
}
}
+ def factoryResetSystemView(viewId: ViewId, callContext: Option[CallContext]): Future[View] = {
+ Future(Views.views.vend.factoryResetSystemView(viewId)) map {
+ unboxFullOrFail(_, callContext, s"$SystemViewNotFound Current ViewId is ${viewId.value}", 404)
+ }
+ }
+
def checkOwnerViewAccessAndReturnOwnerView(user: User, bankAccountId: BankIdAccountId, callContext: Option[CallContext]): Future[View] = {
Future {
user.checkOwnerViewAccessAndReturnOwnerView(bankAccountId, callContext)
diff --git a/obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala
index c0c58318ec..6b2a0e38b7 100644
--- a/obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala
+++ b/obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala
@@ -394,8 +394,15 @@ object JSONFactory1_4_0 extends MdcLoggable{
.find(_.title.toLowerCase.contains(s"${parameter.toLowerCase}"))
.map(_.title).getOrElse("").replaceAll(" ","-")
case _ =>
+ // First try exact match (e.g. body field "address" → glossary item "address").
+ // If that fails, fall back to a dotted-suffix match so body fields like
+ // "bank_id" / "account_id" / "customer_id" resolve to "Bank.bank_id" /
+ // "Account.account_id" / "Customer.customer_id". Without this fallback
+ // body-field glossary links render as [field](/glossary#) with an empty
+ // anchor — see the v5 → v6 createBank rename for a real-world example.
glossaryItems
.find(_.title.toLowerCase.equals(s"${parameter.toLowerCase}"))
+ .orElse(glossaryItems.find(_.title.toLowerCase.endsWith(s".${parameter.toLowerCase}")))
.map(_.title).getOrElse("").replaceAll(" ","-")
}
}
diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
index 59ff9e6397..8b8b3d76b6 100644
--- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
+++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
@@ -2221,7 +2221,25 @@ trait APIMethods600 {
cc =>
implicit val ec = EndpointContext(Some(cc))
val failMsg = s"$InvalidJsonFormat The Json body should be the $PostBankJson600 "
+ // Catch the common v4/v5 → v6 field-rename mistakes before extraction.
+ // Without this, sending {"id": "..."} silently produces an empty bank_id
+ // and fails downstream with a confusing BANK_ID length-validation error.
+ val deprecatedFieldHints: List[String] = json match {
+ case JObject(fields) => fields.collect {
+ case JField("id", _) =>
+ "'id' was renamed to 'bank_id' in v6.0.0 (it was the field name in v4.0.0 and v5.0.0)"
+ case JField("short_name", _) =>
+ "'short_name' was removed in v5.0.0 (it only existed in v4.0.0)"
+ }
+ case _ => Nil
+ }
for {
+ _ <- Helper.booleanToFuture(
+ failMsg = s"$InvalidJsonFormat Deprecated request-body field(s): ${deprecatedFieldHints.mkString("; ")}. The v6.0.0 createBank body shape is: bank_id, bank_code, full_name, logo, website, bank_routings.",
+ cc = cc.callContext
+ ) {
+ deprecatedFieldHints.isEmpty
+ }
postJson <- NewStyle.function.tryons(failMsg, 400, cc.callContext) {
json.extract[PostBankJson600]
}
diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala
index 7acd976e56..260bc83195 100644
--- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala
+++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala
@@ -8,7 +8,7 @@ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
import code.api.ResourceDocs1_4_0.{ResourceDocs140, ResourceDocsAPIMethodsUtil}
import code.api.util.APIUtil.{EmptyBody, _}
import code.api.util.{APIUtil, ApiRole, ApiVersionUtils, CallContext, CustomJsonFormats, Glossary, NewStyle}
-import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, canCreateEntitlementAtOneBank, canCreateOrganisation, canCreateRoutingScheme, canDeleteEntitlementAtAnyBank, canDeleteOrganisation, canDeleteRoutingScheme, canGetAccountAccessTrace, canGetAnyOrganisation, canGetAnyUser, canGetCacheConfig, canGetCacheInfo, canGetCacheNamespaces, canGetConnectorHealth, canGetCustomersAtOneBank, canGetDatabasePoolInfo, canGetMigrations, canUpdateBankSupportedRoutingScheme, canUpdateOrganisation, canUpdateRoutingScheme}
+import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, canCreateEntitlementAtOneBank, canCreateOrganisation, canCreateRoutingScheme, canDeleteEntitlementAtAnyBank, canDeleteOrganisation, canDeleteRoutingScheme, canGetAccountAccessTrace, canGetAnyOrganisation, canGetAnyUser, canGetCacheConfig, canGetCacheInfo, canGetCacheNamespaces, canGetConnectorHealth, canGetCustomersAtOneBank, canGetDatabasePoolInfo, canGetMigrations, canUpdateBankSupportedRoutingScheme, canUpdateOrganisation, canUpdateRoutingScheme, canUpdateSystemView}
import code.api.util.ApiTag._
import code.api.util.ErrorMessages._
import code.api.util.http4s.{ErrorResponseConverter, Http4sRequestAttributes, IdempotencyMiddleware, RequestScopeConnection, ResourceDocMiddleware}
@@ -18,6 +18,7 @@ import code.api.v1_4_0.JSONFactory1_4_0
import code.api.v2_0_0.{BasicViewJson, CreateEntitlementJSON, JSONFactory200}
import code.api.v4_0_0.JSONFactory400
import code.api.v6_0_0.{BasicAccountJsonV600, BasicAccountsJsonV600, BankJsonV600, CacheConfigJsonV600, CacheInfoJsonV600, CacheNamespaceInfoJsonV600, CacheNamespaceJsonV600, CacheNamespacesJsonV600, ConnectorInfoJsonV600, ConnectorsJsonV600, DatabasePoolInfoJsonV600, FeaturesJsonV600, InMemoryCacheStatusJsonV600, JSONFactory600, RedisCacheStatusJsonV600, StoredProcedureConnectorHealthJsonV600, UserV600}
+import code.api.v6_0_0.JSONFactory600.ViewJsonV600
import code.api.cache.Redis
import code.bankconnectors.storedprocedure.StoredProcedureUtils
import code.migration.MigrationScriptLogProvider
@@ -150,7 +151,7 @@ object Http4s700 {
}
}
- object Implementations7_0_0 {
+ object Implementations7_0_0 extends code.util.Helper.MdcLoggable {
// Common prefix: /obp/v7.0.0
val prefixPath = Root / ApiPathZero.toString / implementedInApiVersion.toString
@@ -3437,6 +3438,87 @@ object Http4s700 {
// ── End BULK ──────────────────────────────────────────────────────────────
// ── Test-only rollback endpoint ───────────────────────────────────────────
+ // Route: POST /obp/v7.0.0/management/system-views/VIEW_ID/factory-reset
+ //
+ // Reset an existing system view's permissions and view-level flags to the
+ // code-defined defaults. The ViewDefinition row is preserved so any
+ // AccountAccess records that reference this view remain valid — only the
+ // contents of the view are wiped and rewritten.
+ //
+ // Each successful invocation is audit-logged at INFO level with the
+ // calling user_id and the reset view_id; this is a high-impact admin
+ // action and we want a trace of who reset what.
+ val factoryResetSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] {
+ case req @ POST -> `prefixPath` / "management" / "system-views" / viewIdStr / "factory-reset" =>
+ EndpointHelpers.withUser(req) { (user, cc) =>
+ val viewId = ViewId(viewIdStr)
+ for {
+ view <- ViewNewStyle.factoryResetSystemView(viewId, Some(cc))
+ } yield {
+ logger.info(
+ s"AUDIT factoryResetSystemView: user_id=${user.userId} provider=${user.provider} " +
+ s"view_id=${viewId.value} permissions_count=${view.allowed_actions.size}"
+ )
+ JSONFactory600.createViewJsonV600(view)
+ }
+ }
+ }
+
+ resourceDocs += ResourceDoc(
+ null,
+ implementedInApiVersion,
+ nameOf(factoryResetSystemView),
+ "POST",
+ "/management/system-views/VIEW_ID/factory-reset",
+ "Factory Reset a System View",
+ s"""Reset the system view identified by VIEW_ID to the code-defined defaults.
+ |
+ |This wipes the view's existing permissions and re-applies whatever the
+ |running OBP-API code currently defines as the default permission set
+ |for that system view id. View-level flags (name, description, is_firehose,
+ |alias settings, is_public) are also restored to defaults.
+ |
+ |The underlying view row is preserved, so any AccountAccess records that
+ |grant users this view on specific accounts remain in place — only the
+ |contents of the view itself are reset.
+ |
+ |Each successful invocation is audit-logged with the calling user_id and
+ |the reset view_id.
+ |
+ |${userAuthenticationMessage(true)}""".stripMargin,
+ EmptyBody,
+ ViewJsonV600(
+ bank_id = "",
+ account_id = "",
+ view_id = "auditor",
+ view_name = "Auditor",
+ description = "auditor",
+ metadata_view = "",
+ is_public = false,
+ is_system = true,
+ is_firehose = Some(false),
+ alias = "",
+ hide_metadata_if_alias_used = false,
+ can_grant_access_to_views = Nil,
+ can_revoke_access_to_views = Nil,
+ allowed_actions = List(
+ "can_see_bank_account_balance",
+ "can_see_transaction_amount",
+ "can_add_comment",
+ "can_add_tag"
+ )
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ UserHasMissingRoles,
+ SystemViewNotFound,
+ UnknownError
+ ),
+ apiTagSystemView :: Nil,
+ Some(List(canUpdateSystemView)),
+ http4sPartialFunction = Some(factoryResetSystemView)
+ )
+
// Enabled only in Lift test mode (Props.testMode == true, i.e. -Drun.mode=test).
// Props.testMode is set from the JVM system property before any props file loads,
// so it is reliably available at object-initialization time unlike file-based props.
diff --git a/obp-api/src/main/scala/code/views/MapperViews.scala b/obp-api/src/main/scala/code/views/MapperViews.scala
index ab020b3367..e6ad15947a 100644
--- a/obp-api/src/main/scala/code/views/MapperViews.scala
+++ b/obp-api/src/main/scala/code/views/MapperViews.scala
@@ -903,75 +903,121 @@ object MapperViews extends Views with MdcLoggable {
.usePrivateAliasIfOneExists_(false) //(default is false anyways)
.usePublicAliasIfOneExists_(false) //(default is false anyways)
.hideOtherAccountMetadataIfAlias_(false) //(default is false anyways)
-
+ applyDefaultsForSystemView(entity, viewId)
+ }
+
+ /**
+ * Apply the code-defined default permissions (and any view-level flags such
+ * as isFirehose) for the given system view id to the supplied entity.
+ *
+ * Called both at initial creation (`unsavedSystemView`) and on factory
+ * reset (`factoryResetSystemView`), so the two paths stay in lock-step.
+ *
+ * The caller is responsible for having already cleared any pre-existing
+ * `ViewPermission` rows associated with this view — `resetViewPermissions`
+ * here only repopulates them.
+ */
+ def applyDefaultsForSystemView(entity: ViewDefinition, viewId: String): ViewDefinition = {
viewId match {
- case SYSTEM_OWNER_VIEW_ID | SYSTEM_STANDARD_VIEW_ID =>{
+ case SYSTEM_OWNER_VIEW_ID | SYSTEM_STANDARD_VIEW_ID =>
ViewPermission.resetViewPermissions(
entity,
- SYSTEM_OWNER_VIEW_PERMISSION_ADMIN ++SYSTEM_VIEW_PERMISSION_COMMON,
+ SYSTEM_OWNER_VIEW_PERMISSION_ADMIN ++ SYSTEM_VIEW_PERMISSION_COMMON,
DEFAULT_CAN_GRANT_AND_REVOKE_ACCESS_TO_VIEWS,
DEFAULT_CAN_GRANT_AND_REVOKE_ACCESS_TO_VIEWS
)
- entity
- }
- case SYSTEM_STAGE_ONE_VIEW_ID =>{
+ entity
+ case SYSTEM_STAGE_ONE_VIEW_ID =>
ViewPermission.resetViewPermissions(
entity,
- SYSTEM_VIEW_PERMISSION_COMMON++SYSTEM_VIEW_PERMISSION_COMMON
+ SYSTEM_VIEW_PERMISSION_COMMON ++ SYSTEM_VIEW_PERMISSION_COMMON
)
entity
- }
- case SYSTEM_MANAGE_CUSTOM_VIEWS_VIEW_ID =>{
+ case SYSTEM_MANAGE_CUSTOM_VIEWS_VIEW_ID =>
ViewPermission.resetViewPermissions(
entity,
- SYSTEM_VIEW_PERMISSION_COMMON++SYSTEM_MANAGER_VIEW_PERMISSION
+ SYSTEM_VIEW_PERMISSION_COMMON ++ SYSTEM_MANAGER_VIEW_PERMISSION
)
entity
- }
- case SYSTEM_FIREHOSE_VIEW_ID =>{
+ case SYSTEM_FIREHOSE_VIEW_ID =>
ViewPermission.resetViewPermissions(
entity,
SYSTEM_VIEW_PERMISSION_COMMON
)
- entity // Make additional setup to the existing view
- .isFirehose_(true)
- }
- case SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID |
+ entity.isFirehose_(true)
+ case SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID |
SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID =>
entity
- case SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID =>{
+ case SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID =>
ViewPermission.resetViewPermissions(
entity,
SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_PERMISSION
)
entity
- }
- case SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID =>{
+ case SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID =>
ViewPermission.resetViewPermissions(
entity,
SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_PERMISSION
)
entity
- }
+ case SYSTEM_AUDITOR_VIEW_ID =>
+ ViewPermission.resetViewPermissions(
+ entity,
+ SYSTEM_AUDITOR_VIEW_PERMISSION
+ )
+ entity
case SYSTEM_ACCOUNTANT_VIEW_ID |
- SYSTEM_AUDITOR_VIEW_ID |
SYSTEM_READ_ACCOUNTS_BASIC_VIEW_ID |
SYSTEM_READ_ACCOUNTS_DETAIL_VIEW_ID |
SYSTEM_READ_BALANCES_VIEW_ID |
SYSTEM_READ_TRANSACTIONS_BASIC_VIEW_ID |
SYSTEM_READ_TRANSACTIONS_DEBITS_VIEW_ID |
- SYSTEM_READ_TRANSACTIONS_DETAIL_VIEW_ID => {
-
+ SYSTEM_READ_TRANSACTIONS_DETAIL_VIEW_ID =>
ViewPermission.resetViewPermissions(
entity,
SYSTEM_VIEW_PERMISSION_COMMON
)
entity
- }
case _ =>
entity
}
}
+
+ /**
+ * Reset an existing system view to the code-defined defaults — i.e. wipe
+ * all of its `ViewPermission` rows, restore the view-level fields
+ * (name, description, flags) to what `unsavedSystemView` would set, and
+ * re-apply the default permission set for this view id.
+ *
+ * Preserves the underlying `ViewDefinition` row (and therefore any
+ * `AccountAccess` records pointing at it) — only the contents are reset.
+ *
+ * Returns Empty if no system view with this id exists; the caller can
+ * then surface the standard `SystemViewNotFound` error.
+ */
+ def factoryResetSystemView(viewId: ViewId): Box[View] = {
+ ViewDefinition.findSystemView(viewId.value) match {
+ case Full(existing) =>
+ ViewPermission.findSystemViewPermissions(viewId).foreach(_.delete_!)
+ existing
+ .isSystem_(true)
+ .isFirehose_(false)
+ .bank_id(null)
+ .account_id(null)
+ .name_(StringHelpers.capify(viewId.value))
+ .view_id(viewId.value)
+ .description_(viewId.value)
+ .isPublic_(false)
+ .usePrivateAliasIfOneExists_(false)
+ .usePublicAliasIfOneExists_(false)
+ .hideOtherAccountMetadataIfAlias_(false)
+ applyDefaultsForSystemView(existing, viewId.value)
+ Full(existing.saveMe())
+ case Empty =>
+ Empty
+ case f: Failure => f
+ }
+ }
def createAndSaveSystemView(viewId: String) : Box[View] = {
logger.debug(s"-->createAndSaveSystemView.viewId.start${viewId} ")
diff --git a/obp-api/src/main/scala/code/views/Views.scala b/obp-api/src/main/scala/code/views/Views.scala
index 1627f90a81..4a462272bb 100644
--- a/obp-api/src/main/scala/code/views/Views.scala
+++ b/obp-api/src/main/scala/code/views/Views.scala
@@ -100,6 +100,14 @@ trait Views {
def getOrCreateSystemView(viewId: String) : Box[View]
def getOrCreateCustomPublicView(bankId: BankId, accountId: AccountId, description: String) : Box[View]
+ /**
+ * Reset an existing system view's permissions and view-level flags back to
+ * the code-defined defaults for that view id. Preserves the row itself
+ * (so any AccountAccess bindings keep working). Returns Empty if no such
+ * system view exists.
+ */
+ def factoryResetSystemView(viewId: ViewId) : Box[View]
+
def getOwners(view: View): Set[User]
def removeAllAccountAccess(bankId: BankId, accountId: AccountId) : Boolean
diff --git a/obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala
index 31c2432798..6d49afe965 100644
--- a/obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala
+++ b/obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala
@@ -6,8 +6,12 @@ import code.api.util.http4s.Http4sLiftWebBridge
import code.api.Constant.SYSTEM_OWNER_VIEW_ID
import code.api.ResponseHeader
import code.api.util.APIUtil
-import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, canCreateOrganisation, canCreateRoutingScheme, canDeleteEntitlementAtAnyBank, canDeleteOrganisation, canDeleteRoutingScheme, canGetAccountAccessTrace, canGetAnyOrganisation, canGetAnyUser, canGetCacheConfig, canGetCacheInfo, canGetCacheNamespaces, canGetCardsForBank, canGetConnectorHealth, canGetCustomersAtOneBank, canGetDatabasePoolInfo, canGetMigrations, canReadResourceDoc, canUpdateBankSupportedRoutingScheme, canUpdateOrganisation, canUpdateRoutingScheme}
-import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, BankNotFound, EntitlementAlreadyExists, InvalidOrganisationIdFormat, InvalidRoutingSchemeName, MobileWalletDestinationNotFound, MobileWalletInvalidMsisdn, OrganisationAlreadyExists, OrganisationNotFound, PayeeLookupAddressMismatch, PayeeLookupIdentifierTypeNotRegistered, PayeeNotFound, RoutingSchemeAlreadyExists, RoutingSchemeExampleAddressMismatch, RoutingSchemeNotFound, UserHasMissingRoles, UserNotFoundByUserId}
+import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, canCreateOrganisation, canCreateRoutingScheme, canDeleteEntitlementAtAnyBank, canDeleteOrganisation, canDeleteRoutingScheme, canUpdateSystemView, canGetAccountAccessTrace, canGetAnyOrganisation, canGetAnyUser, canGetCacheConfig, canGetCacheInfo, canGetCacheNamespaces, canGetCardsForBank, canGetConnectorHealth, canGetCustomersAtOneBank, canGetDatabasePoolInfo, canGetMigrations, canReadResourceDoc, canUpdateBankSupportedRoutingScheme, canUpdateOrganisation, canUpdateRoutingScheme}
+import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, BankNotFound, EntitlementAlreadyExists, InvalidOrganisationIdFormat, InvalidRoutingSchemeName, MobileWalletDestinationNotFound, MobileWalletInvalidMsisdn, OrganisationAlreadyExists, OrganisationNotFound, PayeeLookupAddressMismatch, PayeeLookupIdentifierTypeNotRegistered, PayeeNotFound, RoutingSchemeAlreadyExists, RoutingSchemeExampleAddressMismatch, RoutingSchemeNotFound, SystemViewNotFound, UserHasMissingRoles, UserNotFoundByUserId}
+import code.api.Constant.SYSTEM_AUDITOR_VIEW_ID
+import code.views.MapperViews
+import code.views.system.ViewPermission
+import com.openbankproject.commons.model.ViewId
import code.routingscheme.RoutingSchemes
import code.model.dataAccess.BankAccountRouting
import code.customer.CustomerX
@@ -2964,4 +2968,101 @@ class Http4s700RoutesTest extends ServerSetupWithTestData {
}
}
+ // ─── factoryResetSystemView ───────────────────────────────────────────────
+
+ feature("Http4s700 factoryResetSystemView endpoint") {
+
+ scenario("Reject unauthenticated POST to /management/system-views/VIEW_ID/factory-reset", Http4s700RoutesTag) {
+ Given("POST /obp/v7.0.0/management/system-views/auditor/factory-reset with no auth")
+ val (statusCode, json, _) = makeHttpRequestWithBody(
+ "POST", "/obp/v7.0.0/management/system-views/auditor/factory-reset", "")
+
+ Then("Response is 401")
+ statusCode shouldBe 401
+ json match {
+ case JObject(fields) =>
+ toFieldMap(fields).get("message") match {
+ case Some(JString(msg)) => msg should include(AuthenticatedUserIsRequired)
+ case _ => fail("Expected message field")
+ }
+ case _ => fail("Expected JSON object")
+ }
+ }
+
+ scenario("Return 403 when authenticated but missing canUpdateSystemView role", Http4s700RoutesTag) {
+ Given("POST /obp/v7.0.0/management/system-views/auditor/factory-reset without the required role")
+ val headers = Map("DirectLogin" -> s"token=${token1.value}")
+ val (statusCode, json, _) = makeHttpRequestWithBody(
+ "POST", "/obp/v7.0.0/management/system-views/auditor/factory-reset", "", headers)
+
+ Then("Response is 403 with UserHasMissingRoles message naming the required role")
+ statusCode shouldBe 403
+ json match {
+ case JObject(fields) =>
+ toFieldMap(fields).get("message") match {
+ case Some(JString(msg)) =>
+ msg should include(UserHasMissingRoles)
+ msg should include(canUpdateSystemView.toString)
+ case _ => fail("Expected message field")
+ }
+ case _ => fail("Expected JSON object")
+ }
+ }
+
+ scenario("Return 200 and reset permissions when entitled and view exists", Http4s700RoutesTag) {
+ Given("the auditor system view exists, with an extra non-default permission")
+ MapperViews.getOrCreateSystemView(SYSTEM_AUDITOR_VIEW_ID)
+ ViewPermission.createSystemViewPermission(
+ ViewId(SYSTEM_AUDITOR_VIEW_ID),
+ code.api.Constant.CAN_ADD_TRANSACTION_REQUEST_TO_OWN_ACCOUNT,
+ None
+ )
+ addEntitlement("", resourceUser1.userId, canUpdateSystemView.toString)
+
+ When(s"POST /obp/v7.0.0/management/system-views/$SYSTEM_AUDITOR_VIEW_ID/factory-reset is called")
+ val headers = Map("DirectLogin" -> s"token=${token1.value}")
+ val (statusCode, json, _) = makeHttpRequestWithBody(
+ "POST", s"/obp/v7.0.0/management/system-views/$SYSTEM_AUDITOR_VIEW_ID/factory-reset", "", headers)
+
+ Then("Response is 200 with the refreshed view JSON, no longer containing the extra permission")
+ statusCode shouldBe 200
+ json match {
+ case JObject(fields) =>
+ val map = toFieldMap(fields)
+ map.get("view_id") match {
+ case Some(JString(v)) => v shouldBe SYSTEM_AUDITOR_VIEW_ID
+ case _ => fail("Expected view_id as JSON string")
+ }
+ map.get("allowed_actions") match {
+ case Some(JArray(actions)) =>
+ val names = actions.collect { case JString(s) => s }
+ names should not contain code.api.Constant.CAN_ADD_TRANSACTION_REQUEST_TO_OWN_ACCOUNT
+ case _ => fail("Expected allowed_actions array")
+ }
+ case _ => fail("Expected JSON object")
+ }
+ }
+
+ scenario("Return 404 when system view does not exist", Http4s700RoutesTag) {
+ Given("canUpdateSystemView role granted and a non-existent view id")
+ addEntitlement("", resourceUser1.userId, canUpdateSystemView.toString)
+
+ When("POST /obp/v7.0.0/management/system-views/does-not-exist/factory-reset")
+ val headers = Map("DirectLogin" -> s"token=${token1.value}")
+ val (statusCode, json, _) = makeHttpRequestWithBody(
+ "POST", "/obp/v7.0.0/management/system-views/does-not-exist/factory-reset", "", headers)
+
+ Then("Response is 404 with SystemViewNotFound message")
+ statusCode shouldBe 404
+ json match {
+ case JObject(fields) =>
+ toFieldMap(fields).get("message") match {
+ case Some(JString(msg)) => msg should include(SystemViewNotFound)
+ case _ => fail("Expected message field")
+ }
+ case _ => fail("Expected JSON object")
+ }
+ }
+ }
+
}
diff --git a/obp-api/src/test/scala/code/views/MappedViewsTest.scala b/obp-api/src/test/scala/code/views/MappedViewsTest.scala
index 55f2736772..35f58da174 100644
--- a/obp-api/src/test/scala/code/views/MappedViewsTest.scala
+++ b/obp-api/src/test/scala/code/views/MappedViewsTest.scala
@@ -3,9 +3,9 @@ package code.views
import code.api.Constant
import code.api.util.ErrorMessages.ViewIdNotSupported
import code.setup.{DefaultUsers, ServerSetup}
-import code.views.system.ViewDefinition
-import com.openbankproject.commons.model.{AccountId, BankId, BankIdAccountId}
-import net.liftweb.common.Failure
+import code.views.system.{ViewDefinition, ViewPermission}
+import com.openbankproject.commons.model.{AccountId, BankId, BankIdAccountId, ViewId}
+import net.liftweb.common.{Empty, Failure}
class MappedViewsTest extends ServerSetup with DefaultUsers{
@@ -63,11 +63,40 @@ class MappedViewsTest extends ServerSetup with DefaultUsers{
wrongView.toString contains ViewIdNotSupported shouldBe (true)
wrongView.toString contains wrongViewId shouldBe(true)
-
+
}
-
-
-
+
+ scenario("factoryResetSystemView restores code-defined defaults") {
+ Given("an existing auditor system view created by getOrCreateSystemView")
+ val created = MapperViews.getOrCreateSystemView(viewIdAuditor)
+ created.isDefined shouldBe true
+ val defaultActions = created.openOrThrowException("auditor view should exist").allowed_actions
+ defaultActions.contains(Constant.CAN_ADD_TRANSACTION_REQUEST_TO_OWN_ACCOUNT) shouldBe false
+
+ When("we add an extra permission that's not in the default auditor set")
+ ViewPermission.createSystemViewPermission(
+ ViewId(viewIdAuditor),
+ Constant.CAN_ADD_TRANSACTION_REQUEST_TO_OWN_ACCOUNT,
+ None
+ ).isDefined shouldBe true
+ val mutated = ViewDefinition.findSystemView(viewIdAuditor)
+ .openOrThrowException("auditor view should still exist after mutation")
+ mutated.allowed_actions.contains(Constant.CAN_ADD_TRANSACTION_REQUEST_TO_OWN_ACCOUNT) shouldBe true
+
+ Then("factoryResetSystemView removes the extra permission and restores defaults")
+ val reset = MapperViews.factoryResetSystemView(ViewId(viewIdAuditor))
+ reset.isDefined shouldBe true
+ val resetActions = reset.openOrThrowException("reset should return refreshed view").allowed_actions
+ resetActions.contains(Constant.CAN_ADD_TRANSACTION_REQUEST_TO_OWN_ACCOUNT) shouldBe false
+ resetActions.toSet should equal(defaultActions.toSet)
+ }
+
+ scenario("factoryResetSystemView returns Empty for an unknown system view id") {
+ MapperViews.factoryResetSystemView(ViewId("does-not-exist")) shouldBe Empty
+ }
+
+
+
}
diff --git a/pom.xml b/pom.xml
index 6c3c6266e5..bb30fd5925 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,8 @@
2.12
2.12.20
- 1.1.2
+ 1.1.5
+ 1.1.0
1.8.2
3.5.0
0.23.30
@@ -73,6 +74,20 @@
pom
import
+
+
+ net.minidev
+ json-smart
+ 2.6.0
+
+
+ net.minidev
+ accessors-smart
+ 2.6.0
+
com.tesobe
obp-commons