Skip to content

Commit b492e1f

Browse files
authored
Merge pull request #2782 from simonredfern/develop
Restrict auditor view. Factory reset system view endpoint
2 parents 2c4ec07 + cf6e47f commit b492e1f

12 files changed

Lines changed: 442 additions & 37 deletions

File tree

obp-api/pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@
225225
<dependency>
226226
<groupId>org.apache.pekko</groupId>
227227
<artifactId>pekko-http-core_${scala.version}</artifactId>
228-
<version>1.1.0</version>
228+
<version>${pekko-http.version}</version>
229229
</dependency>
230230
<dependency>
231231
<groupId>org.apache.pekko</groupId>
@@ -304,6 +304,12 @@
304304
<artifactId>oauth2-oidc-sdk</artifactId>
305305
<version>11.37.1</version>
306306
</dependency>
307+
<!-- json-smart version forced to 2.6.0 via parent pom dependencyManagement
308+
(fixes CVE-2023-1370). -->
309+
<dependency>
310+
<groupId>net.minidev</groupId>
311+
<artifactId>json-smart</artifactId>
312+
</dependency>
307313
<!-- ********** flexmark START ********** -->
308314
<!-- Library flexmark-all v0.40.8 is replaced with used modules -->
309315
<!-- https://mvnrepository.com/artifact/com.vladsch.flexmark/flexmark-profile-pegdown -->

obp-api/src/main/scala/code/api/constant/constant.scala

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,78 @@ object Constant extends MdcLoggable {
586586
CAN_ANSWER_TRANSACTION_REQUEST_CHALLENGE
587587
)
588588

589+
// Auditor system view: read-only on the account itself. The auditor can
590+
// see everything but cannot modify account data, counterparties, images,
591+
// locations, aliases, URLs, or initiate / approve payments.
592+
// The only writes allowed are the auditor's own annotations: comments and
593+
// tags (including where_tags). Those are scoped to the auditor's view as
594+
// separate metadata — they do not change the account or its data.
595+
final val SYSTEM_AUDITOR_VIEW_PERMISSION = List(
596+
// See transactions
597+
CAN_SEE_TRANSACTION_THIS_BANK_ACCOUNT,
598+
CAN_SEE_TRANSACTION_OTHER_BANK_ACCOUNT,
599+
CAN_SEE_TRANSACTION_METADATA,
600+
CAN_SEE_TRANSACTION_AMOUNT,
601+
CAN_SEE_TRANSACTION_TYPE,
602+
CAN_SEE_TRANSACTION_CURRENCY,
603+
CAN_SEE_TRANSACTION_START_DATE,
604+
CAN_SEE_TRANSACTION_FINISH_DATE,
605+
CAN_SEE_TRANSACTION_BALANCE,
606+
CAN_SEE_TRANSACTION_STATUS,
607+
// See this account
608+
CAN_SEE_BANK_ACCOUNT_OWNERS,
609+
CAN_SEE_BANK_ACCOUNT_TYPE,
610+
CAN_SEE_BANK_ACCOUNT_BALANCE,
611+
CAN_SEE_BANK_ACCOUNT_CURRENCY,
612+
CAN_SEE_BANK_ACCOUNT_LABEL,
613+
CAN_SEE_BANK_ACCOUNT_NATIONAL_IDENTIFIER,
614+
CAN_SEE_BANK_ACCOUNT_SWIFT_BIC,
615+
CAN_SEE_BANK_ACCOUNT_IBAN,
616+
CAN_SEE_BANK_ACCOUNT_NUMBER,
617+
CAN_SEE_BANK_ACCOUNT_BANK_NAME,
618+
CAN_SEE_BANK_ACCOUNT_BANK_PERMALINK,
619+
CAN_SEE_BANK_ACCOUNT_ROUTING_SCHEME,
620+
CAN_SEE_BANK_ACCOUNT_ROUTING_ADDRESS,
621+
// See bank
622+
CAN_SEE_BANK_ROUTING_SCHEME,
623+
CAN_SEE_BANK_ROUTING_ADDRESS,
624+
// See other accounts
625+
CAN_SEE_OTHER_ACCOUNT_NATIONAL_IDENTIFIER,
626+
CAN_SEE_OTHER_ACCOUNT_SWIFT_BIC,
627+
CAN_SEE_OTHER_ACCOUNT_IBAN,
628+
CAN_SEE_OTHER_ACCOUNT_BANK_NAME,
629+
CAN_SEE_OTHER_ACCOUNT_NUMBER,
630+
CAN_SEE_OTHER_ACCOUNT_METADATA,
631+
CAN_SEE_OTHER_ACCOUNT_KIND,
632+
CAN_SEE_OTHER_ACCOUNT_ROUTING_SCHEME,
633+
CAN_SEE_OTHER_ACCOUNT_ROUTING_ADDRESS,
634+
CAN_SEE_OTHER_BANK_ROUTING_SCHEME,
635+
CAN_SEE_OTHER_BANK_ROUTING_ADDRESS,
636+
// See metadata (everyone's)
637+
CAN_SEE_COMMENTS,
638+
CAN_SEE_OWNER_COMMENT,
639+
CAN_SEE_TAGS,
640+
CAN_SEE_WHERE_TAG,
641+
CAN_SEE_IMAGES,
642+
CAN_SEE_MORE_INFO,
643+
CAN_SEE_URL,
644+
CAN_SEE_IMAGE_URL,
645+
CAN_SEE_OPEN_CORPORATES_URL,
646+
CAN_SEE_CORPORATE_LOCATION,
647+
CAN_SEE_PHYSICAL_LOCATION,
648+
CAN_SEE_PUBLIC_ALIAS,
649+
CAN_SEE_PRIVATE_ALIAS,
650+
// Read counterparties (no modification)
651+
CAN_GET_COUNTERPARTY,
652+
// Auditor's own annotations only
653+
CAN_ADD_COMMENT,
654+
CAN_DELETE_COMMENT,
655+
CAN_ADD_TAG,
656+
CAN_DELETE_TAG,
657+
CAN_ADD_WHERE_TAG,
658+
CAN_DELETE_WHERE_TAG
659+
)
660+
589661
final val ALL_VIEW_PERMISSION_NAMES = List(
590662
CAN_SEE_TRANSACTION_OTHER_BANK_ACCOUNT,
591663
CAN_SEE_TRANSACTION_METADATA,

obp-api/src/main/scala/code/api/util/Glossary.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,21 @@ object Glossary extends MdcLoggable {
939939
|
940940
|
941941
|Example value: ${bankIdExample.value}
942+
|
943+
|## Version history
944+
|
945+
|The JSON field name for this identifier changed across OBP-API versions:
946+
|
947+
|- **v6.0.0+** (current): `bank_id` — the canonical field name in both request and response bodies (e.g. `PostBankJson600`, `BankJson600`).
948+
|- **v5.0.0**: `id` (Option[String]) — see `PostBankJson500` / `BankJson500`.
949+
|- **v4.0.0**: `id` (String), plus a now-removed `short_name` field — see `PostBankJson400` / `BankJson400`.
950+
|
951+
|The v6 createBank request body shape is exactly:
952+
|`bank_id`, `bank_code`, `full_name`, `logo`, `website`, `bank_routings`.
953+
|
954+
|If you're regenerating client code from older docs, samples, or LLM training data, double-check
955+
|the field name — sending `id` to v6 endpoints will silently produce an empty `bank_id` and
956+
|fail validation with a confusing length error.
942957
""")
943958

944959
glossaryItems += GlossaryItem(

obp-api/src/main/scala/code/api/util/newstyle/ViewNewStyle.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ object ViewNewStyle {
103103
}
104104
}
105105

106+
def factoryResetSystemView(viewId: ViewId, callContext: Option[CallContext]): Future[View] = {
107+
Future(Views.views.vend.factoryResetSystemView(viewId)) map {
108+
unboxFullOrFail(_, callContext, s"$SystemViewNotFound Current ViewId is ${viewId.value}", 404)
109+
}
110+
}
111+
106112
def checkOwnerViewAccessAndReturnOwnerView(user: User, bankAccountId: BankIdAccountId, callContext: Option[CallContext]): Future[View] = {
107113
Future {
108114
user.checkOwnerViewAccessAndReturnOwnerView(bankAccountId, callContext)

obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,15 @@ object JSONFactory1_4_0 extends MdcLoggable{
394394
.find(_.title.toLowerCase.contains(s"${parameter.toLowerCase}"))
395395
.map(_.title).getOrElse("").replaceAll(" ","-")
396396
case _ =>
397+
// First try exact match (e.g. body field "address" → glossary item "address").
398+
// If that fails, fall back to a dotted-suffix match so body fields like
399+
// "bank_id" / "account_id" / "customer_id" resolve to "Bank.bank_id" /
400+
// "Account.account_id" / "Customer.customer_id". Without this fallback
401+
// body-field glossary links render as [field](/glossary#) with an empty
402+
// anchor — see the v5 → v6 createBank rename for a real-world example.
397403
glossaryItems
398404
.find(_.title.toLowerCase.equals(s"${parameter.toLowerCase}"))
405+
.orElse(glossaryItems.find(_.title.toLowerCase.endsWith(s".${parameter.toLowerCase}")))
399406
.map(_.title).getOrElse("").replaceAll(" ","-")
400407
}
401408
}

obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,7 +2221,25 @@ trait APIMethods600 {
22212221
cc =>
22222222
implicit val ec = EndpointContext(Some(cc))
22232223
val failMsg = s"$InvalidJsonFormat The Json body should be the $PostBankJson600 "
2224+
// Catch the common v4/v5 → v6 field-rename mistakes before extraction.
2225+
// Without this, sending {"id": "..."} silently produces an empty bank_id
2226+
// and fails downstream with a confusing BANK_ID length-validation error.
2227+
val deprecatedFieldHints: List[String] = json match {
2228+
case JObject(fields) => fields.collect {
2229+
case JField("id", _) =>
2230+
"'id' was renamed to 'bank_id' in v6.0.0 (it was the field name in v4.0.0 and v5.0.0)"
2231+
case JField("short_name", _) =>
2232+
"'short_name' was removed in v5.0.0 (it only existed in v4.0.0)"
2233+
}
2234+
case _ => Nil
2235+
}
22242236
for {
2237+
_ <- Helper.booleanToFuture(
2238+
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.",
2239+
cc = cc.callContext
2240+
) {
2241+
deprecatedFieldHints.isEmpty
2242+
}
22252243
postJson <- NewStyle.function.tryons(failMsg, 400, cc.callContext) {
22262244
json.extract[PostBankJson600]
22272245
}

obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
88
import code.api.ResourceDocs1_4_0.{ResourceDocs140, ResourceDocsAPIMethodsUtil}
99
import code.api.util.APIUtil.{EmptyBody, _}
1010
import code.api.util.{APIUtil, ApiRole, ApiVersionUtils, CallContext, CustomJsonFormats, Glossary, NewStyle}
11-
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}
11+
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}
1212
import code.api.util.ApiTag._
1313
import code.api.util.ErrorMessages._
1414
import code.api.util.http4s.{ErrorResponseConverter, Http4sRequestAttributes, IdempotencyMiddleware, RequestScopeConnection, ResourceDocMiddleware}
@@ -18,6 +18,7 @@ import code.api.v1_4_0.JSONFactory1_4_0
1818
import code.api.v2_0_0.{BasicViewJson, CreateEntitlementJSON, JSONFactory200}
1919
import code.api.v4_0_0.JSONFactory400
2020
import code.api.v6_0_0.{BasicAccountJsonV600, BasicAccountsJsonV600, BankJsonV600, CacheConfigJsonV600, CacheInfoJsonV600, CacheNamespaceInfoJsonV600, CacheNamespaceJsonV600, CacheNamespacesJsonV600, ConnectorInfoJsonV600, ConnectorsJsonV600, DatabasePoolInfoJsonV600, FeaturesJsonV600, InMemoryCacheStatusJsonV600, JSONFactory600, RedisCacheStatusJsonV600, StoredProcedureConnectorHealthJsonV600, UserV600}
21+
import code.api.v6_0_0.JSONFactory600.ViewJsonV600
2122
import code.api.cache.Redis
2223
import code.bankconnectors.storedprocedure.StoredProcedureUtils
2324
import code.migration.MigrationScriptLogProvider
@@ -150,7 +151,7 @@ object Http4s700 {
150151
}
151152
}
152153

153-
object Implementations7_0_0 {
154+
object Implementations7_0_0 extends code.util.Helper.MdcLoggable {
154155

155156
// Common prefix: /obp/v7.0.0
156157
val prefixPath = Root / ApiPathZero.toString / implementedInApiVersion.toString
@@ -3437,6 +3438,87 @@ object Http4s700 {
34373438
// ── End BULK ──────────────────────────────────────────────────────────────
34383439

34393440
// ── Test-only rollback endpoint ───────────────────────────────────────────
3441+
// Route: POST /obp/v7.0.0/management/system-views/VIEW_ID/factory-reset
3442+
//
3443+
// Reset an existing system view's permissions and view-level flags to the
3444+
// code-defined defaults. The ViewDefinition row is preserved so any
3445+
// AccountAccess records that reference this view remain valid — only the
3446+
// contents of the view are wiped and rewritten.
3447+
//
3448+
// Each successful invocation is audit-logged at INFO level with the
3449+
// calling user_id and the reset view_id; this is a high-impact admin
3450+
// action and we want a trace of who reset what.
3451+
val factoryResetSystemView: HttpRoutes[IO] = HttpRoutes.of[IO] {
3452+
case req @ POST -> `prefixPath` / "management" / "system-views" / viewIdStr / "factory-reset" =>
3453+
EndpointHelpers.withUser(req) { (user, cc) =>
3454+
val viewId = ViewId(viewIdStr)
3455+
for {
3456+
view <- ViewNewStyle.factoryResetSystemView(viewId, Some(cc))
3457+
} yield {
3458+
logger.info(
3459+
s"AUDIT factoryResetSystemView: user_id=${user.userId} provider=${user.provider} " +
3460+
s"view_id=${viewId.value} permissions_count=${view.allowed_actions.size}"
3461+
)
3462+
JSONFactory600.createViewJsonV600(view)
3463+
}
3464+
}
3465+
}
3466+
3467+
resourceDocs += ResourceDoc(
3468+
null,
3469+
implementedInApiVersion,
3470+
nameOf(factoryResetSystemView),
3471+
"POST",
3472+
"/management/system-views/VIEW_ID/factory-reset",
3473+
"Factory Reset a System View",
3474+
s"""Reset the system view identified by VIEW_ID to the code-defined defaults.
3475+
|
3476+
|This wipes the view's existing permissions and re-applies whatever the
3477+
|running OBP-API code currently defines as the default permission set
3478+
|for that system view id. View-level flags (name, description, is_firehose,
3479+
|alias settings, is_public) are also restored to defaults.
3480+
|
3481+
|The underlying view row is preserved, so any AccountAccess records that
3482+
|grant users this view on specific accounts remain in place — only the
3483+
|contents of the view itself are reset.
3484+
|
3485+
|Each successful invocation is audit-logged with the calling user_id and
3486+
|the reset view_id.
3487+
|
3488+
|${userAuthenticationMessage(true)}""".stripMargin,
3489+
EmptyBody,
3490+
ViewJsonV600(
3491+
bank_id = "",
3492+
account_id = "",
3493+
view_id = "auditor",
3494+
view_name = "Auditor",
3495+
description = "auditor",
3496+
metadata_view = "",
3497+
is_public = false,
3498+
is_system = true,
3499+
is_firehose = Some(false),
3500+
alias = "",
3501+
hide_metadata_if_alias_used = false,
3502+
can_grant_access_to_views = Nil,
3503+
can_revoke_access_to_views = Nil,
3504+
allowed_actions = List(
3505+
"can_see_bank_account_balance",
3506+
"can_see_transaction_amount",
3507+
"can_add_comment",
3508+
"can_add_tag"
3509+
)
3510+
),
3511+
List(
3512+
$AuthenticatedUserIsRequired,
3513+
UserHasMissingRoles,
3514+
SystemViewNotFound,
3515+
UnknownError
3516+
),
3517+
apiTagSystemView :: Nil,
3518+
Some(List(canUpdateSystemView)),
3519+
http4sPartialFunction = Some(factoryResetSystemView)
3520+
)
3521+
34403522
// Enabled only in Lift test mode (Props.testMode == true, i.e. -Drun.mode=test).
34413523
// Props.testMode is set from the JVM system property before any props file loads,
34423524
// so it is reliably available at object-initialization time unlike file-based props.

0 commit comments

Comments
 (0)