mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 09:26:53 +00:00
Merge pull request #2675 from simonredfern/develop
v6.0.0 get transactions, get OIDC client and verify
This commit is contained in:
commit
8900d60ead
@ -282,6 +282,12 @@ object ApiRole extends MdcLoggable{
|
||||
case class CanGetCurrentConsumer(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetCurrentConsumer = CanGetCurrentConsumer()
|
||||
|
||||
case class CanVerifyOidcClient(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canVerifyOidcClient = CanVerifyOidcClient()
|
||||
|
||||
case class CanGetOidcClient(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetOidcClient = CanGetOidcClient()
|
||||
|
||||
case class CanCreateTransactionType(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canCreateTransactionType = CanCreateTransactionType()
|
||||
|
||||
|
||||
@ -23,20 +23,22 @@ import code.api.v3_0_0.JSONFactory300
|
||||
import code.api.v3_0_0.JSONFactory300.createAggregateMetricJson
|
||||
import code.api.v2_0_0.JSONFactory200
|
||||
import code.api.v3_1_0.{JSONFactory310, PostCustomerNumberJsonV310}
|
||||
import code.api.v4_0_0.CallLimitPostJsonV400
|
||||
import code.api.v1_2_1.{AccountHolderJSON, BankRoutingJsonV121, TransactionDetailsJSON}
|
||||
import code.api.v4_0_0.{BankAttributeBankResponseJsonV400, CallLimitPostJsonV400}
|
||||
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.{AddUserToGroupResponseJsonV600, DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupEntitlementJsonV600, GroupEntitlementsJsonV600, GroupJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, UserGroupMembershipJsonV600, UserGroupMembershipsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createAbacRuleJsonV600, createAbacRulesJsonV600, createActiveRateLimitsJsonV600, createCallLimitJsonV600, createRedisCallCountersJson}
|
||||
import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CacheConfigJsonV600, CacheInfoJsonV600, CacheNamespaceInfoJsonV600, CreateAbacRuleJsonV600, CreateDynamicEntityRequestJsonV600, CurrentConsumerJsonV600, DynamicEntityDefinitionJsonV600, DynamicEntityDefinitionWithCountJsonV600, DynamicEntitiesWithCountJsonV600, DynamicEntityLinksJsonV600, ExecuteAbacRuleJsonV600, InMemoryCacheStatusJsonV600, MyDynamicEntitiesJsonV600, PostVerifyUserCredentialsJsonV600, RedisCacheStatusJsonV600, RelatedLinkJsonV600, UpdateAbacRuleJsonV600, UpdateDynamicEntityRequestJsonV600}
|
||||
import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CacheConfigJsonV600, CacheInfoJsonV600, CacheNamespaceInfoJsonV600, CreateAbacRuleJsonV600, CreateDynamicEntityRequestJsonV600, CurrentConsumerJsonV600, DynamicEntityDefinitionJsonV600, DynamicEntityDefinitionWithCountJsonV600, DynamicEntitiesWithCountJsonV600, DynamicEntityLinksJsonV600, ExecuteAbacRuleJsonV600, GetOidcClientResponseJsonV600, InMemoryCacheStatusJsonV600, MyDynamicEntitiesJsonV600, PostVerifyUserCredentialsJsonV600, RedisCacheStatusJsonV600, RelatedLinkJsonV600, UpdateAbacRuleJsonV600, UpdateDynamicEntityRequestJsonV600, VerifyOidcClientRequestJsonV600, VerifyOidcClientResponseJsonV600}
|
||||
import code.api.v6_0_0.OBPAPI6_0_0
|
||||
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
|
||||
import code.metrics.APIMetrics
|
||||
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
|
||||
import code.bankconnectors.storedprocedure.StoredProcedureUtils
|
||||
import code.bankconnectors.LocalMappedConnectorInternal._
|
||||
import code.consumer.Consumers
|
||||
import code.entitlement.Entitlement
|
||||
import code.loginattempts.LoginAttempt
|
||||
import code.model._
|
||||
@ -917,6 +919,174 @@ trait APIMethods600 {
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
getBanks,
|
||||
implementedInApiVersion,
|
||||
nameOf(getBanks),
|
||||
"GET",
|
||||
"/banks",
|
||||
"Get Banks",
|
||||
"""Get banks on this API instance
|
||||
|Returns a list of banks supported on this server:
|
||||
|
|
||||
|- bank_id used as parameter in URLs
|
||||
|- Short and full name of bank
|
||||
|- Logo URL
|
||||
|- Website
|
||||
|
|
||||
|User Authentication is Optional. The User need not be logged in.
|
||||
|""",
|
||||
EmptyBody,
|
||||
BanksJsonV600(List(BankJsonV600(
|
||||
bank_id = "gh.29.uk",
|
||||
bank_code = "bank_code",
|
||||
full_name = "full_name",
|
||||
logo = "logo",
|
||||
website = "www.openbankproject.com",
|
||||
bank_routings = List(BankRoutingJsonV121("OBP", "gh.29.uk")),
|
||||
attributes = Some(List(BankAttributeBankResponseJsonV400("OVERDRAFT_LIMIT", "1000")))
|
||||
))),
|
||||
List(UnknownError),
|
||||
apiTagBank :: apiTagPSD2AIS :: apiTagPsd2 :: Nil
|
||||
)
|
||||
|
||||
lazy val getBanks: OBPEndpoint = {
|
||||
case "banks" :: Nil JsonGet _ => { cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
(banks, callContext) <- NewStyle.function.getBanks(cc.callContext)
|
||||
} yield {
|
||||
(JSONFactory600.createBanksJsonV600(banks), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
getBank,
|
||||
implementedInApiVersion,
|
||||
nameOf(getBank),
|
||||
"GET",
|
||||
"/banks/BANK_ID",
|
||||
"Get Bank",
|
||||
"""Get the bank specified by BANK_ID
|
||||
|Returns information about a single bank specified by BANK_ID including:
|
||||
|
|
||||
|- bank_id: The unique identifier of this bank
|
||||
|- Short and full name of bank
|
||||
|- Logo URL
|
||||
|- Website
|
||||
|""",
|
||||
EmptyBody,
|
||||
BankJsonV600(
|
||||
bank_id = "gh.29.uk",
|
||||
bank_code = "bank_code",
|
||||
full_name = "full_name",
|
||||
logo = "logo",
|
||||
website = "www.openbankproject.com",
|
||||
bank_routings = List(BankRoutingJsonV121("OBP", "gh.29.uk")),
|
||||
attributes = Some(List(BankAttributeBankResponseJsonV400("OVERDRAFT_LIMIT", "1000")))
|
||||
),
|
||||
List(UnknownError, BankNotFound),
|
||||
apiTagBank :: apiTagPSD2AIS :: apiTagPsd2 :: Nil
|
||||
)
|
||||
|
||||
lazy val getBank: OBPEndpoint = {
|
||||
case "banks" :: BankId(bankId) :: Nil JsonGet _ => { cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
(bank, callContext) <- NewStyle.function.getBank(bankId, cc.callContext)
|
||||
(attributes, callContext) <- NewStyle.function.getBankAttributesByBank(bankId, callContext)
|
||||
} yield {
|
||||
(JSONFactory600.createBankJsonV600(bank, attributes), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
getTransactionsForBankAccount,
|
||||
implementedInApiVersion,
|
||||
nameOf(getTransactionsForBankAccount),
|
||||
"GET",
|
||||
"/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions",
|
||||
"Get Transactions for Account (Full)",
|
||||
s"""Returns transactions list of the account specified by ACCOUNT_ID and [moderated](#1_2_1-getViewsForBankAccount) by the view (VIEW_ID).
|
||||
|
|
||||
|${userAuthenticationMessage(false)}
|
||||
|
|
||||
|Authentication is required if the view is not public.
|
||||
|
|
||||
|${urlParametersDocument(true, true)}
|
||||
|
|
||||
|**Note:** This v6.0.0 endpoint returns `bank_id` directly in both `this_account` and `other_account` objects,
|
||||
|making it easier to identify which bank each account belongs to without parsing the `bank_routing` object.
|
||||
|
|
||||
|""",
|
||||
EmptyBody,
|
||||
TransactionsJsonV600(List(TransactionJsonV600(
|
||||
transaction_id = "123",
|
||||
this_account = ThisAccountJsonV600(
|
||||
bank_id = "gh.29.uk",
|
||||
account_id = "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0",
|
||||
bank_routing = BankRoutingJsonV121("OBP", "gh.29.uk"),
|
||||
account_routings = List(AccountRoutingJsonV121("OBP", "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0")),
|
||||
holders = List(AccountHolderJSON("John Doe", false))
|
||||
),
|
||||
other_account = OtherAccountJsonV600(
|
||||
bank_id = "other.bank.uk",
|
||||
account_id = "counterparty-123",
|
||||
holder = AccountHolderJSON("Jane Smith", false),
|
||||
bank_routing = BankRoutingJsonV121("OBP", "other.bank.uk"),
|
||||
account_routings = List(AccountRoutingJsonV121("OBP", "counterparty-123")),
|
||||
metadata = null
|
||||
),
|
||||
details = TransactionDetailsJSON(
|
||||
`type` = "SEPA",
|
||||
description = "Payment for services",
|
||||
posted = new java.util.Date(),
|
||||
completed = new java.util.Date(),
|
||||
new_balance = AmountOfMoneyJsonV121("EUR", "1000.00"),
|
||||
value = AmountOfMoneyJsonV121("EUR", "100.00")
|
||||
),
|
||||
metadata = null,
|
||||
transaction_attributes = Nil
|
||||
))),
|
||||
List(
|
||||
FilterSortDirectionError,
|
||||
FilterOffersetError,
|
||||
FilterLimitError,
|
||||
FilterDateFormatError,
|
||||
AuthenticatedUserIsRequired,
|
||||
BankAccountNotFound,
|
||||
ViewNotFound,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagTransaction, apiTagAccount)
|
||||
)
|
||||
|
||||
lazy val getTransactionsForBankAccount: OBPEndpoint = {
|
||||
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: Nil JsonGet req => {
|
||||
cc => implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
(user, callContext) <- authenticatedAccess(cc)
|
||||
(bank, callContext) <- NewStyle.function.getBank(bankId, callContext)
|
||||
(bankAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, accountId, callContext)
|
||||
view <- ViewNewStyle.checkViewAccessAndReturnView(viewId, BankIdAccountId(bankAccount.bankId, bankAccount.accountId), user, callContext)
|
||||
(params, callContext) <- createQueriesByHttpParamsFuture(callContext.get.requestHeaders, callContext)
|
||||
(transactions, callContext) <- bankAccount.getModeratedTransactionsFuture(bank, user, view, callContext, params) map {
|
||||
connectorEmptyResponse(_, callContext)
|
||||
}
|
||||
moderatedTransactionsWithAttributes <- Future.sequence(transactions.map(transaction =>
|
||||
NewStyle.function.getTransactionAttributes(
|
||||
bankId,
|
||||
transaction.id,
|
||||
cc.callContext: Option[CallContext]).map(attributes => code.api.v3_0_0.ModeratedTransactionWithAttributes(transaction, attributes._1))
|
||||
))
|
||||
} yield {
|
||||
(JSONFactory600.createTransactionsJsonV600(moderatedTransactionsWithAttributes), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy val getCurrentConsumer: OBPEndpoint = {
|
||||
case "consumers" :: "current" :: Nil JsonGet _ => {
|
||||
cc => {
|
||||
@ -1644,8 +1814,9 @@ trait APIMethods600 {
|
||||
json.extract[PostBankJson600]
|
||||
}
|
||||
|
||||
// TODO: Improve this error message to not hardcode "16" - should reference the max length from checkOptionalShortString function
|
||||
checkShortStringValue = APIUtil.checkOptionalShortString(postJson.bank_id)
|
||||
_ <- Helper.booleanToFuture(failMsg = s"$checkShortStringValue.", cc = cc.callContext) {
|
||||
_ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat BANK_ID: $checkShortStringValue BANK_ID must contain only characters A-Z, a-z, 0-9, -, _, . and be max 16 characters.", cc = cc.callContext) {
|
||||
checkShortStringValue == SILENCE_IS_GOLDEN
|
||||
}
|
||||
|
||||
@ -7221,6 +7392,135 @@ trait APIMethods600 {
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
verifyOidcClient,
|
||||
implementedInApiVersion,
|
||||
nameOf(verifyOidcClient),
|
||||
"POST",
|
||||
"/oidc/clients/verify",
|
||||
"Verify OIDC Client",
|
||||
s"""Verifies an OIDC/OAuth2 client's credentials.
|
||||
|
|
||||
|Returns `valid: true` if the client_id and client_secret match an active consumer.
|
||||
|Also returns the consumer_id and redirect_uris for use by the OIDC provider.
|
||||
|
|
||||
|${userAuthenticationMessage(true)}
|
||||
|""",
|
||||
VerifyOidcClientRequestJsonV600(
|
||||
client_id = "abc123def456",
|
||||
client_secret = "supersecret123"
|
||||
),
|
||||
VerifyOidcClientResponseJsonV600(
|
||||
valid = true,
|
||||
client_id = Some("abc123def456"),
|
||||
consumer_id = Some("7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh"),
|
||||
redirect_uris = Some(List("https://app.example.com/callback"))
|
||||
),
|
||||
List(
|
||||
$AuthenticatedUserIsRequired,
|
||||
UserHasMissingRoles,
|
||||
InvalidJsonFormat,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagOIDC, apiTagConsumer, apiTagOAuth),
|
||||
Some(List(canVerifyOidcClient))
|
||||
)
|
||||
|
||||
lazy val verifyOidcClient: OBPEndpoint = {
|
||||
case "oidc" :: "clients" :: "verify" :: Nil JsonPost json -> _ => {
|
||||
cc => implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(cc)
|
||||
_ <- if(isSuperAdmin(u.userId)) Future.successful(Full(Unit))
|
||||
else NewStyle.function.hasEntitlement("", u.userId, canVerifyOidcClient, callContext)
|
||||
postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the VerifyOidcClientRequestJsonV600", 400, callContext) {
|
||||
json.extract[VerifyOidcClientRequestJsonV600]
|
||||
}
|
||||
consumerBox <- Future {
|
||||
Consumers.consumers.vend.getConsumerByConsumerKey(postedData.client_id)
|
||||
}
|
||||
} yield {
|
||||
consumerBox match {
|
||||
case Full(consumer) if consumer.isActive.get && consumer.secret.get == postedData.client_secret =>
|
||||
val redirectUris = Option(consumer.redirectURL.get)
|
||||
.filter(_.nonEmpty)
|
||||
.map(_.split("[,\\s]+").map(_.trim).filter(_.nonEmpty).toList)
|
||||
(VerifyOidcClientResponseJsonV600(
|
||||
valid = true,
|
||||
client_id = Some(postedData.client_id),
|
||||
consumer_id = Some(consumer.consumerId.get),
|
||||
redirect_uris = redirectUris
|
||||
), HttpCode.`200`(callContext))
|
||||
case _ =>
|
||||
(VerifyOidcClientResponseJsonV600(valid = false), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
getOidcClient,
|
||||
implementedInApiVersion,
|
||||
nameOf(getOidcClient),
|
||||
"GET",
|
||||
"/oidc/clients/CLIENT_ID",
|
||||
"Get OIDC Client",
|
||||
s"""Gets an OIDC/OAuth2 client's metadata by client_id.
|
||||
|
|
||||
|Returns client information including name, consumer_id, redirect_uris, and enabled status.
|
||||
|This endpoint does not verify the client secret - use POST /oidc/clients/verify for authentication.
|
||||
|
|
||||
|${userAuthenticationMessage(true)}
|
||||
|""",
|
||||
EmptyBody,
|
||||
GetOidcClientResponseJsonV600(
|
||||
client_id = "abc123def456",
|
||||
client_name = "My Application",
|
||||
consumer_id = "7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh",
|
||||
redirect_uris = List("https://app.example.com/callback"),
|
||||
enabled = true
|
||||
),
|
||||
List(
|
||||
$AuthenticatedUserIsRequired,
|
||||
UserHasMissingRoles,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagOIDC, apiTagConsumer, apiTagOAuth),
|
||||
Some(List(canGetOidcClient))
|
||||
)
|
||||
|
||||
lazy val getOidcClient: OBPEndpoint = {
|
||||
case "oidc" :: "clients" :: clientId :: Nil JsonGet _ => {
|
||||
cc => implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(cc)
|
||||
_ <- if(isSuperAdmin(u.userId)) Future.successful(Full(Unit))
|
||||
else NewStyle.function.hasEntitlement("", u.userId, canGetOidcClient, callContext)
|
||||
consumerBox <- Future {
|
||||
Consumers.consumers.vend.getConsumerByConsumerKey(clientId)
|
||||
}
|
||||
consumer <- NewStyle.function.tryons(s"OBP-OIDC-003: Client not found: $clientId", 404, callContext) {
|
||||
consumerBox match {
|
||||
case Full(c) => c
|
||||
case _ => throw new RuntimeException("Client not found")
|
||||
}
|
||||
}
|
||||
} yield {
|
||||
val redirectUris = Option(consumer.redirectURL.get)
|
||||
.filter(_.nonEmpty)
|
||||
.map(_.split("[,\\s]+").map(_.trim).filter(_.nonEmpty).toList)
|
||||
.getOrElse(List.empty)
|
||||
(GetOidcClientResponseJsonV600(
|
||||
client_id = clientId,
|
||||
client_name = consumer.name.get,
|
||||
consumer_id = consumer.consumerId.get,
|
||||
redirect_uris = redirectUris,
|
||||
enabled = consumer.isActive.get
|
||||
), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,17 +16,19 @@ package code.api.v6_0_0
|
||||
import code.api.util.APIUtil.stringOrNull
|
||||
import code.api.util.RateLimitingPeriod.LimitCallPeriod
|
||||
import code.api.util._
|
||||
import code.api.v1_2_1.BankRoutingJsonV121
|
||||
import code.api.v1_2_1.{AccountHolderJSON, BankRoutingJsonV121, OtherAccountMetadataJSON, TransactionDetailsJSON, TransactionMetadataJSON}
|
||||
import code.api.v1_4_0.JSONFactory1_4_0.CustomerFaceImageJson
|
||||
import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200}
|
||||
import code.api.v2_1_0.CustomerCreditRatingJSON
|
||||
import code.api.v3_0_0.{
|
||||
CustomerAttributeResponseJsonV300,
|
||||
ModeratedTransactionWithAttributes,
|
||||
UserJsonV300,
|
||||
ViewJSON300,
|
||||
ViewsJSON300
|
||||
}
|
||||
import code.api.v3_1_0.{RateLimit, RedisCallLimitJson}
|
||||
import code.api.v3_1_0.{AccountAttributeResponseJson, RateLimit, RedisCallLimitJson}
|
||||
import code.api.v4_0_0.TransactionAttributeResponseJson
|
||||
import code.api.v4_0_0.{BankAttributeBankResponseJsonV400, UserAgreementJson}
|
||||
import code.entitlement.Entitlement
|
||||
import code.loginattempts.LoginAttempt
|
||||
@ -76,6 +78,28 @@ case class CurrentConsumerJsonV600(
|
||||
call_counters: RedisCallCountersJsonV600
|
||||
)
|
||||
|
||||
// OIDC Client Verification models (V600)
|
||||
case class VerifyOidcClientRequestJsonV600(
|
||||
client_id: String,
|
||||
client_secret: String
|
||||
)
|
||||
|
||||
case class VerifyOidcClientResponseJsonV600(
|
||||
valid: Boolean,
|
||||
client_id: Option[String] = None,
|
||||
consumer_id: Option[String] = None,
|
||||
redirect_uris: Option[List[String]] = None
|
||||
)
|
||||
|
||||
// OIDC Client Get (metadata lookup without secret verification)
|
||||
case class GetOidcClientResponseJsonV600(
|
||||
client_id: String,
|
||||
client_name: String,
|
||||
consumer_id: String,
|
||||
redirect_uris: List[String],
|
||||
enabled: Boolean
|
||||
)
|
||||
|
||||
case class CallLimitPostJsonV600(
|
||||
from_date: java.util.Date,
|
||||
to_date: java.util.Date,
|
||||
@ -334,6 +358,18 @@ case class StoredProcedureConnectorHealthJsonV600(
|
||||
error_message: Option[String]
|
||||
)
|
||||
|
||||
case class BankJsonV600(
|
||||
bank_id: String,
|
||||
bank_code: String,
|
||||
full_name: String,
|
||||
logo: String,
|
||||
website: String,
|
||||
bank_routings: List[BankRoutingJsonV121],
|
||||
attributes: Option[List[BankAttributeBankResponseJsonV400]]
|
||||
)
|
||||
|
||||
case class BanksJsonV600(banks: List[BankJsonV600])
|
||||
|
||||
case class PostCustomerJsonV600(
|
||||
legal_name: String,
|
||||
customer_number: Option[String] = None,
|
||||
@ -515,6 +551,37 @@ case class AbacPoliciesJsonV600(
|
||||
policies: List[AbacPolicyJsonV600]
|
||||
)
|
||||
|
||||
// Transaction JSON structures for v6.0.0 - with bank_id included directly
|
||||
case class ThisAccountJsonV600(
|
||||
bank_id: String,
|
||||
account_id: String,
|
||||
bank_routing: BankRoutingJsonV121,
|
||||
account_routings: List[AccountRoutingJsonV121],
|
||||
holders: List[AccountHolderJSON]
|
||||
)
|
||||
|
||||
case class OtherAccountJsonV600(
|
||||
bank_id: String,
|
||||
account_id: String,
|
||||
holder: AccountHolderJSON,
|
||||
bank_routing: BankRoutingJsonV121,
|
||||
account_routings: List[AccountRoutingJsonV121],
|
||||
metadata: OtherAccountMetadataJSON
|
||||
)
|
||||
|
||||
case class TransactionJsonV600(
|
||||
transaction_id: String,
|
||||
this_account: ThisAccountJsonV600,
|
||||
other_account: OtherAccountJsonV600,
|
||||
details: TransactionDetailsJSON,
|
||||
metadata: TransactionMetadataJSON,
|
||||
transaction_attributes: List[TransactionAttributeResponseJson]
|
||||
)
|
||||
|
||||
case class TransactionsJsonV600(
|
||||
transactions: List[TransactionJsonV600]
|
||||
)
|
||||
|
||||
// HATEOAS-style links for dynamic entity discoverability
|
||||
case class RelatedLinkJsonV600(rel: String, href: String, method: String)
|
||||
case class DynamicEntityLinksJsonV600(
|
||||
@ -1397,6 +1464,34 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
|
||||
)
|
||||
}
|
||||
|
||||
def createBankJsonV600(bank: Bank, attributes: List[BankAttributeTrait] = Nil): BankJsonV600 = {
|
||||
val obp = BankRoutingJsonV121("OBP", bank.bankId.value)
|
||||
val bic = BankRoutingJsonV121("BIC", bank.swiftBic)
|
||||
val routings = bank.bankRoutingScheme match {
|
||||
case "OBP" => bic :: BankRoutingJsonV121(bank.bankRoutingScheme, bank.bankRoutingAddress) :: Nil
|
||||
case "BIC" => obp :: BankRoutingJsonV121(bank.bankRoutingScheme, bank.bankRoutingAddress) :: Nil
|
||||
case _ => obp :: bic :: BankRoutingJsonV121(bank.bankRoutingScheme, bank.bankRoutingAddress) :: Nil
|
||||
}
|
||||
BankJsonV600(
|
||||
bank_id = stringOrNull(bank.bankId.value),
|
||||
bank_code = stringOrNull(bank.shortName),
|
||||
full_name = stringOrNull(bank.fullName),
|
||||
logo = stringOrNull(bank.logoUrl),
|
||||
website = stringOrNull(bank.websiteUrl),
|
||||
bank_routings = routings.filter(a => stringOrNull(a.address) != null),
|
||||
attributes = Option(
|
||||
attributes.filter(_.isActive == Some(true)).map(a => BankAttributeBankResponseJsonV400(
|
||||
name = a.name,
|
||||
value = a.value)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def createBanksJsonV600(banks: List[Bank]): BanksJsonV600 = {
|
||||
BanksJsonV600(banks.map(bank => createBankJsonV600(bank, Nil)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create v6.0.0 response for GET /my/dynamic-entities
|
||||
*
|
||||
@ -1557,4 +1652,75 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
|
||||
)
|
||||
}
|
||||
|
||||
// Transaction v6.0.0 factory methods
|
||||
|
||||
import code.api.util.APIUtil.stringOptionOrNull
|
||||
import code.api.v1_2_1.JSONFactory.{createAmountOfMoneyJSON, createTransactionCommentJSON, createTransactionTagJSON, createTransactionImageJSON, createLocationJSON, createAccountHolderJSON}
|
||||
import code.api.v3_0_0.JSONFactory300.createOtherAccountMetaDataJSON
|
||||
import code.api.v4_0_0.JSONFactory400.createTransactionAttributeJson
|
||||
import code.model.{ModeratedBankAccount, ModeratedOtherBankAccount, ModeratedTransaction, ModeratedTransactionMetadata}
|
||||
|
||||
def createTransactionsJsonV600(moderatedTransactionsWithAttributes: List[ModeratedTransactionWithAttributes]): TransactionsJsonV600 = {
|
||||
TransactionsJsonV600(moderatedTransactionsWithAttributes.map(t => createTransactionJsonV600(t.transaction, t.transactionAttributes)))
|
||||
}
|
||||
|
||||
def createTransactionJsonV600(transaction: ModeratedTransaction, transactionAttributes: List[TransactionAttribute]): TransactionJsonV600 = {
|
||||
TransactionJsonV600(
|
||||
transaction_id = transaction.id.value,
|
||||
this_account = transaction.bankAccount.map(createThisAccountJsonV600).getOrElse(null),
|
||||
other_account = transaction.otherBankAccount.map(createOtherAccountJsonV600).getOrElse(null),
|
||||
details = createTransactionDetailsJsonV600(transaction),
|
||||
metadata = transaction.metadata.map(createTransactionMetadataJsonV600).getOrElse(null),
|
||||
transaction_attributes = transactionAttributes.map(createTransactionAttributeJson)
|
||||
)
|
||||
}
|
||||
|
||||
def createThisAccountJsonV600(bankAccount: ModeratedBankAccount): ThisAccountJsonV600 = {
|
||||
ThisAccountJsonV600(
|
||||
bank_id = bankAccount.bankId.value,
|
||||
account_id = bankAccount.accountId.value,
|
||||
bank_routing = BankRoutingJsonV121(stringOptionOrNull(bankAccount.bankRoutingScheme), stringOptionOrNull(bankAccount.bankRoutingAddress)),
|
||||
account_routings = List(AccountRoutingJsonV121(stringOptionOrNull(bankAccount.accountRoutingScheme), stringOptionOrNull(bankAccount.accountRoutingAddress))),
|
||||
holders = bankAccount.owners.map(x => x.toList.map(holder => AccountHolderJSON(name = holder.name, is_alias = false))).getOrElse(null)
|
||||
)
|
||||
}
|
||||
|
||||
def createOtherAccountJsonV600(bankAccount: ModeratedOtherBankAccount): OtherAccountJsonV600 = {
|
||||
// Extract bank_id from bank_routing when scheme is "OBP", otherwise use the address as best effort
|
||||
val bankId = bankAccount.bankRoutingScheme match {
|
||||
case Some("OBP") => stringOptionOrNull(bankAccount.bankRoutingAddress)
|
||||
case _ => stringOptionOrNull(bankAccount.bankRoutingAddress) // Best effort - use address
|
||||
}
|
||||
|
||||
OtherAccountJsonV600(
|
||||
bank_id = bankId,
|
||||
account_id = bankAccount.id,
|
||||
holder = createAccountHolderJSON(bankAccount.label.display, bankAccount.isAlias),
|
||||
bank_routing = BankRoutingJsonV121(stringOptionOrNull(bankAccount.bankRoutingScheme), stringOptionOrNull(bankAccount.bankRoutingAddress)),
|
||||
account_routings = List(AccountRoutingJsonV121(stringOptionOrNull(bankAccount.accountRoutingScheme), stringOptionOrNull(bankAccount.accountRoutingAddress))),
|
||||
metadata = bankAccount.metadata.map(createOtherAccountMetaDataJSON).getOrElse(null)
|
||||
)
|
||||
}
|
||||
|
||||
def createTransactionDetailsJsonV600(transaction: ModeratedTransaction): TransactionDetailsJSON = {
|
||||
TransactionDetailsJSON(
|
||||
`type` = stringOptionOrNull(transaction.transactionType),
|
||||
description = stringOptionOrNull(transaction.description),
|
||||
posted = transaction.startDate.getOrElse(null),
|
||||
completed = transaction.finishDate.getOrElse(null),
|
||||
new_balance = createAmountOfMoneyJSON(transaction.currency, transaction.balance),
|
||||
value = createAmountOfMoneyJSON(transaction.currency, transaction.amount.map(_.toString))
|
||||
)
|
||||
}
|
||||
|
||||
def createTransactionMetadataJsonV600(metadata: ModeratedTransactionMetadata): TransactionMetadataJSON = {
|
||||
TransactionMetadataJSON(
|
||||
narrative = stringOptionOrNull(metadata.ownerComment),
|
||||
comments = metadata.comments.map(_.map(createTransactionCommentJSON)).getOrElse(null),
|
||||
tags = metadata.tags.map(_.map(createTransactionTagJSON)).getOrElse(null),
|
||||
images = metadata.images.map(_.map(createTransactionImageJSON)).getOrElse(null),
|
||||
where = metadata.whereTag.map(createLocationJSON).getOrElse(null)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -207,16 +207,30 @@ class AuthUser extends MegaProtoUser[AuthUser] with CreatedUpdated with MdcLogga
|
||||
*/
|
||||
def valUniqueExternally(msg: => String)(uniqueUsername: String): List[FieldError] ={
|
||||
if (APIUtil.getPropsAsBoolValue("connector.user.authentication", false)) {
|
||||
Connector.connector.vend.checkExternalUserExists(uniqueUsername, None).map(_.sub) match {
|
||||
logger.info(s"valUniqueExternally: calling checkExternalUserExists for username: $uniqueUsername")
|
||||
val connectorResult = Connector.connector.vend.checkExternalUserExists(uniqueUsername, None)
|
||||
logger.info(s"valUniqueExternally: checkExternalUserExists returned: ${connectorResult.getClass.getSimpleName}")
|
||||
connectorResult.map(_.sub) match {
|
||||
case Full(returnedUsername) => // Get the username via connector
|
||||
logger.info(s"valUniqueExternally: checkExternalUserExists returned username: $returnedUsername")
|
||||
if(uniqueUsername == returnedUsername) { // Username is NOT unique
|
||||
logger.info(s"valUniqueExternally: username $uniqueUsername already exists externally")
|
||||
List(FieldError(this, Text(msg))) // provide the error message
|
||||
} else {
|
||||
} else {
|
||||
logger.info(s"valUniqueExternally: username $uniqueUsername is unique (returned different: $returnedUsername)")
|
||||
Nil // All good. Allow username creation
|
||||
}
|
||||
case ParamFailure(message,_,_,APIFailure(errorMessage, errorCode)) if errorMessage.contains("NO DATA") => // Cannot get the username via connector
|
||||
logger.info(s"valUniqueExternally: checkExternalUserExists returned NO DATA for username: $uniqueUsername - allowing creation")
|
||||
Nil // All good. Allow username creation
|
||||
case Failure(failureMsg, exception, chain) =>
|
||||
logger.warn(s"valUniqueExternally: checkExternalUserExists failed for username: $uniqueUsername, message: $failureMsg, exception: ${exception.map(_.getMessage)}, chain: $chain")
|
||||
List(FieldError(this, Text(msg)))
|
||||
case Empty =>
|
||||
logger.warn(s"valUniqueExternally: checkExternalUserExists returned Empty for username: $uniqueUsername")
|
||||
List(FieldError(this, Text(msg)))
|
||||
case _ => // Any other case we provide error message
|
||||
logger.warn(s"valUniqueExternally: checkExternalUserExists returned unexpected result for username: $uniqueUsername")
|
||||
List(FieldError(this, Text(msg)))
|
||||
}
|
||||
} else {
|
||||
@ -932,8 +946,12 @@ import net.liftweb.util.Helpers._
|
||||
* @return Return the authUser
|
||||
*/
|
||||
def checkExternalUserViaConnector(username: String, password: String):Box[AuthUser] = {
|
||||
Connector.connector.vend.checkExternalUserCredentials(username, password, None) match {
|
||||
logger.info(s"checkExternalUserViaConnector: calling checkExternalUserCredentials for username: $username")
|
||||
val connectorResult = Connector.connector.vend.checkExternalUserCredentials(username, password, None)
|
||||
logger.info(s"checkExternalUserViaConnector: checkExternalUserCredentials returned: ${connectorResult.getClass.getSimpleName}")
|
||||
connectorResult match {
|
||||
case Full(InboundExternalUser(aud, exp, iat, iss, sub, azp, email, emailVerified, name, userAuthContexts)) =>
|
||||
logger.info(s"checkExternalUserViaConnector: successful response for sub: $sub, iss: $iss, email: $email")
|
||||
val user = findAuthUserByUsernameAndProvider(sub, iss) match { // Check if the external user is already created locally
|
||||
case Full(user) if user.validated_? => // Return existing user if found
|
||||
logger.debug("external user already exists locally, using that one")
|
||||
@ -969,7 +987,14 @@ import net.liftweb.util.Helpers._
|
||||
case None => // Do nothing
|
||||
}
|
||||
Full(user)
|
||||
case Failure(msg, exception, chain) =>
|
||||
logger.warn(s"checkExternalUserViaConnector: checkExternalUserCredentials failed for username: $username, message: $msg, exception: ${exception.map(_.getMessage)}, chain: $chain")
|
||||
Empty
|
||||
case Empty =>
|
||||
logger.warn(s"checkExternalUserViaConnector: checkExternalUserCredentials returned Empty for username: $username")
|
||||
Empty
|
||||
case _ =>
|
||||
logger.warn(s"checkExternalUserViaConnector: checkExternalUserCredentials returned unexpected result for username: $username")
|
||||
Empty
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,11 +6,13 @@ import code.api.util.ApiRole.CanCreateBank
|
||||
import code.api.util.ErrorMessages
|
||||
import code.api.util.ErrorMessages.UserHasMissingRoles
|
||||
import code.api.v6_0_0.APIMethods600.Implementations6_0_0
|
||||
import code.entitlement.Entitlement
|
||||
import code.setup.DefaultUsers
|
||||
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 net.liftweb.util.Helpers.randomString
|
||||
import org.scalatest.Tag
|
||||
|
||||
class BankTests extends V600ServerSetup with DefaultUsers {
|
||||
@ -54,6 +56,70 @@ class BankTests extends V600ServerSetup with DefaultUsers {
|
||||
response.code should equal(403)
|
||||
response.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanCreateBank)
|
||||
}
|
||||
|
||||
scenario("Successfully create a bank with a 16-character bank_id (max length)", ApiEndpoint1, VersionOfApi) {
|
||||
// Add the required entitlement
|
||||
val addedEntitlement = Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateBank.toString)
|
||||
|
||||
// Generate a 16-character bank_id (maximum allowed by checkOptionalShortString validation)
|
||||
val longBankId = "bank." + randomString(11).toLowerCase // 5 + 11 = 16 characters
|
||||
|
||||
When("We create a bank with a 16-character bank_id")
|
||||
val postJson = PostBankJson600(
|
||||
bank_id = longBankId,
|
||||
bank_code = "test_code",
|
||||
full_name = Some("Test Bank with Long ID"),
|
||||
logo = Some("https://example.com/logo.png"),
|
||||
website = Some("https://example.com"),
|
||||
bank_routings = None
|
||||
)
|
||||
val request = (v6_0_0_Request / "banks").POST <@ (user1)
|
||||
val response = try {
|
||||
makePostRequest(request, write(postJson))
|
||||
} finally {
|
||||
// Clean up entitlement
|
||||
Entitlement.entitlement.vend.deleteEntitlement(addedEntitlement)
|
||||
}
|
||||
|
||||
Then("We should get a 201")
|
||||
response.code should equal(201)
|
||||
|
||||
And("The response should contain the bank with the 16-character bank_id")
|
||||
val responseJson = response.body
|
||||
(responseJson \ "bank_id").extract[String] should equal(longBankId)
|
||||
(responseJson \ "bank_id").extract[String].length should equal(16)
|
||||
}
|
||||
|
||||
scenario("Fail to create a bank with bank_id exceeding 16 characters", ApiEndpoint1, VersionOfApi) {
|
||||
// Add the required entitlement
|
||||
val addedEntitlement = Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateBank.toString)
|
||||
|
||||
// Generate a 17-character bank_id (exceeds maximum of 16)
|
||||
val tooLongBankId = "bank." + randomString(12).toLowerCase // 5 + 12 = 17 characters
|
||||
|
||||
When("We try to create a bank with a 17-character bank_id")
|
||||
val postJson = PostBankJson600(
|
||||
bank_id = tooLongBankId,
|
||||
bank_code = "test_code",
|
||||
full_name = Some("Test Bank with Too Long ID"),
|
||||
logo = Some("https://example.com/logo.png"),
|
||||
website = Some("https://example.com"),
|
||||
bank_routings = None
|
||||
)
|
||||
val request = (v6_0_0_Request / "banks").POST <@ (user1)
|
||||
val response = try {
|
||||
makePostRequest(request, write(postJson))
|
||||
} finally {
|
||||
// Clean up entitlement
|
||||
Entitlement.entitlement.vend.deleteEntitlement(addedEntitlement)
|
||||
}
|
||||
|
||||
Then("We should get a 400")
|
||||
response.code should equal(400)
|
||||
|
||||
And("The error message should indicate BANK_ID validation failed")
|
||||
response.body.extract[ErrorMessage].message should include("BANK_ID")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user