Skip to content

Commit 8900d60

Browse files
authored
Merge pull request #2675 from simonredfern/develop
v6.0.0 get transactions, get OIDC client and verify
2 parents 278f810 + 7cec587 commit 8900d60

File tree

5 files changed

+571
-8
lines changed

5 files changed

+571
-8
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ object ApiRole extends MdcLoggable{
282282
case class CanGetCurrentConsumer(requiresBankId: Boolean = false) extends ApiRole
283283
lazy val canGetCurrentConsumer = CanGetCurrentConsumer()
284284

285+
case class CanVerifyOidcClient(requiresBankId: Boolean = false) extends ApiRole
286+
lazy val canVerifyOidcClient = CanVerifyOidcClient()
287+
288+
case class CanGetOidcClient(requiresBankId: Boolean = false) extends ApiRole
289+
lazy val canGetOidcClient = CanGetOidcClient()
290+
285291
case class CanCreateTransactionType(requiresBankId: Boolean = true) extends ApiRole
286292
lazy val canCreateTransactionType = CanCreateTransactionType()
287293

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

Lines changed: 303 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,22 @@ import code.api.v3_0_0.JSONFactory300
2323
import code.api.v3_0_0.JSONFactory300.createAggregateMetricJson
2424
import code.api.v2_0_0.JSONFactory200
2525
import code.api.v3_1_0.{JSONFactory310, PostCustomerNumberJsonV310}
26-
import code.api.v4_0_0.CallLimitPostJsonV400
26+
import code.api.v1_2_1.{AccountHolderJSON, BankRoutingJsonV121, TransactionDetailsJSON}
27+
import code.api.v4_0_0.{BankAttributeBankResponseJsonV400, CallLimitPostJsonV400}
2728
import code.api.v4_0_0.JSONFactory400.createCallsLimitJson
2829
import code.api.v5_0_0.JSONFactory500
2930
import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
3031
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
3132
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
3233
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}
33-
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}
34+
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}
3435
import code.api.v6_0_0.OBPAPI6_0_0
3536
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
3637
import code.metrics.APIMetrics
3738
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
3839
import code.bankconnectors.storedprocedure.StoredProcedureUtils
3940
import code.bankconnectors.LocalMappedConnectorInternal._
41+
import code.consumer.Consumers
4042
import code.entitlement.Entitlement
4143
import code.loginattempts.LoginAttempt
4244
import code.model._
@@ -917,6 +919,174 @@ trait APIMethods600 {
917919
}
918920
}
919921

922+
staticResourceDocs += ResourceDoc(
923+
getBanks,
924+
implementedInApiVersion,
925+
nameOf(getBanks),
926+
"GET",
927+
"/banks",
928+
"Get Banks",
929+
"""Get banks on this API instance
930+
|Returns a list of banks supported on this server:
931+
|
932+
|- bank_id used as parameter in URLs
933+
|- Short and full name of bank
934+
|- Logo URL
935+
|- Website
936+
|
937+
|User Authentication is Optional. The User need not be logged in.
938+
|""",
939+
EmptyBody,
940+
BanksJsonV600(List(BankJsonV600(
941+
bank_id = "gh.29.uk",
942+
bank_code = "bank_code",
943+
full_name = "full_name",
944+
logo = "logo",
945+
website = "www.openbankproject.com",
946+
bank_routings = List(BankRoutingJsonV121("OBP", "gh.29.uk")),
947+
attributes = Some(List(BankAttributeBankResponseJsonV400("OVERDRAFT_LIMIT", "1000")))
948+
))),
949+
List(UnknownError),
950+
apiTagBank :: apiTagPSD2AIS :: apiTagPsd2 :: Nil
951+
)
952+
953+
lazy val getBanks: OBPEndpoint = {
954+
case "banks" :: Nil JsonGet _ => { cc =>
955+
implicit val ec = EndpointContext(Some(cc))
956+
for {
957+
(banks, callContext) <- NewStyle.function.getBanks(cc.callContext)
958+
} yield {
959+
(JSONFactory600.createBanksJsonV600(banks), HttpCode.`200`(callContext))
960+
}
961+
}
962+
}
963+
964+
staticResourceDocs += ResourceDoc(
965+
getBank,
966+
implementedInApiVersion,
967+
nameOf(getBank),
968+
"GET",
969+
"/banks/BANK_ID",
970+
"Get Bank",
971+
"""Get the bank specified by BANK_ID
972+
|Returns information about a single bank specified by BANK_ID including:
973+
|
974+
|- bank_id: The unique identifier of this bank
975+
|- Short and full name of bank
976+
|- Logo URL
977+
|- Website
978+
|""",
979+
EmptyBody,
980+
BankJsonV600(
981+
bank_id = "gh.29.uk",
982+
bank_code = "bank_code",
983+
full_name = "full_name",
984+
logo = "logo",
985+
website = "www.openbankproject.com",
986+
bank_routings = List(BankRoutingJsonV121("OBP", "gh.29.uk")),
987+
attributes = Some(List(BankAttributeBankResponseJsonV400("OVERDRAFT_LIMIT", "1000")))
988+
),
989+
List(UnknownError, BankNotFound),
990+
apiTagBank :: apiTagPSD2AIS :: apiTagPsd2 :: Nil
991+
)
992+
993+
lazy val getBank: OBPEndpoint = {
994+
case "banks" :: BankId(bankId) :: Nil JsonGet _ => { cc =>
995+
implicit val ec = EndpointContext(Some(cc))
996+
for {
997+
(bank, callContext) <- NewStyle.function.getBank(bankId, cc.callContext)
998+
(attributes, callContext) <- NewStyle.function.getBankAttributesByBank(bankId, callContext)
999+
} yield {
1000+
(JSONFactory600.createBankJsonV600(bank, attributes), HttpCode.`200`(callContext))
1001+
}
1002+
}
1003+
}
1004+
1005+
staticResourceDocs += ResourceDoc(
1006+
getTransactionsForBankAccount,
1007+
implementedInApiVersion,
1008+
nameOf(getTransactionsForBankAccount),
1009+
"GET",
1010+
"/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions",
1011+
"Get Transactions for Account (Full)",
1012+
s"""Returns transactions list of the account specified by ACCOUNT_ID and [moderated](#1_2_1-getViewsForBankAccount) by the view (VIEW_ID).
1013+
|
1014+
|${userAuthenticationMessage(false)}
1015+
|
1016+
|Authentication is required if the view is not public.
1017+
|
1018+
|${urlParametersDocument(true, true)}
1019+
|
1020+
|**Note:** This v6.0.0 endpoint returns `bank_id` directly in both `this_account` and `other_account` objects,
1021+
|making it easier to identify which bank each account belongs to without parsing the `bank_routing` object.
1022+
|
1023+
|""",
1024+
EmptyBody,
1025+
TransactionsJsonV600(List(TransactionJsonV600(
1026+
transaction_id = "123",
1027+
this_account = ThisAccountJsonV600(
1028+
bank_id = "gh.29.uk",
1029+
account_id = "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0",
1030+
bank_routing = BankRoutingJsonV121("OBP", "gh.29.uk"),
1031+
account_routings = List(AccountRoutingJsonV121("OBP", "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0")),
1032+
holders = List(AccountHolderJSON("John Doe", false))
1033+
),
1034+
other_account = OtherAccountJsonV600(
1035+
bank_id = "other.bank.uk",
1036+
account_id = "counterparty-123",
1037+
holder = AccountHolderJSON("Jane Smith", false),
1038+
bank_routing = BankRoutingJsonV121("OBP", "other.bank.uk"),
1039+
account_routings = List(AccountRoutingJsonV121("OBP", "counterparty-123")),
1040+
metadata = null
1041+
),
1042+
details = TransactionDetailsJSON(
1043+
`type` = "SEPA",
1044+
description = "Payment for services",
1045+
posted = new java.util.Date(),
1046+
completed = new java.util.Date(),
1047+
new_balance = AmountOfMoneyJsonV121("EUR", "1000.00"),
1048+
value = AmountOfMoneyJsonV121("EUR", "100.00")
1049+
),
1050+
metadata = null,
1051+
transaction_attributes = Nil
1052+
))),
1053+
List(
1054+
FilterSortDirectionError,
1055+
FilterOffersetError,
1056+
FilterLimitError,
1057+
FilterDateFormatError,
1058+
AuthenticatedUserIsRequired,
1059+
BankAccountNotFound,
1060+
ViewNotFound,
1061+
UnknownError
1062+
),
1063+
List(apiTagTransaction, apiTagAccount)
1064+
)
1065+
1066+
lazy val getTransactionsForBankAccount: OBPEndpoint = {
1067+
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transactions" :: Nil JsonGet req => {
1068+
cc => implicit val ec = EndpointContext(Some(cc))
1069+
for {
1070+
(user, callContext) <- authenticatedAccess(cc)
1071+
(bank, callContext) <- NewStyle.function.getBank(bankId, callContext)
1072+
(bankAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, accountId, callContext)
1073+
view <- ViewNewStyle.checkViewAccessAndReturnView(viewId, BankIdAccountId(bankAccount.bankId, bankAccount.accountId), user, callContext)
1074+
(params, callContext) <- createQueriesByHttpParamsFuture(callContext.get.requestHeaders, callContext)
1075+
(transactions, callContext) <- bankAccount.getModeratedTransactionsFuture(bank, user, view, callContext, params) map {
1076+
connectorEmptyResponse(_, callContext)
1077+
}
1078+
moderatedTransactionsWithAttributes <- Future.sequence(transactions.map(transaction =>
1079+
NewStyle.function.getTransactionAttributes(
1080+
bankId,
1081+
transaction.id,
1082+
cc.callContext: Option[CallContext]).map(attributes => code.api.v3_0_0.ModeratedTransactionWithAttributes(transaction, attributes._1))
1083+
))
1084+
} yield {
1085+
(JSONFactory600.createTransactionsJsonV600(moderatedTransactionsWithAttributes), HttpCode.`200`(callContext))
1086+
}
1087+
}
1088+
}
1089+
9201090
lazy val getCurrentConsumer: OBPEndpoint = {
9211091
case "consumers" :: "current" :: Nil JsonGet _ => {
9221092
cc => {
@@ -1644,8 +1814,9 @@ trait APIMethods600 {
16441814
json.extract[PostBankJson600]
16451815
}
16461816

1817+
// TODO: Improve this error message to not hardcode "16" - should reference the max length from checkOptionalShortString function
16471818
checkShortStringValue = APIUtil.checkOptionalShortString(postJson.bank_id)
1648-
_ <- Helper.booleanToFuture(failMsg = s"$checkShortStringValue.", cc = cc.callContext) {
1819+
_ <- 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) {
16491820
checkShortStringValue == SILENCE_IS_GOLDEN
16501821
}
16511822

@@ -7221,6 +7392,135 @@ trait APIMethods600 {
72217392
}
72227393
}
72237394

7395+
staticResourceDocs += ResourceDoc(
7396+
verifyOidcClient,
7397+
implementedInApiVersion,
7398+
nameOf(verifyOidcClient),
7399+
"POST",
7400+
"/oidc/clients/verify",
7401+
"Verify OIDC Client",
7402+
s"""Verifies an OIDC/OAuth2 client's credentials.
7403+
|
7404+
|Returns `valid: true` if the client_id and client_secret match an active consumer.
7405+
|Also returns the consumer_id and redirect_uris for use by the OIDC provider.
7406+
|
7407+
|${userAuthenticationMessage(true)}
7408+
|""",
7409+
VerifyOidcClientRequestJsonV600(
7410+
client_id = "abc123def456",
7411+
client_secret = "supersecret123"
7412+
),
7413+
VerifyOidcClientResponseJsonV600(
7414+
valid = true,
7415+
client_id = Some("abc123def456"),
7416+
consumer_id = Some("7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh"),
7417+
redirect_uris = Some(List("https://app.example.com/callback"))
7418+
),
7419+
List(
7420+
$AuthenticatedUserIsRequired,
7421+
UserHasMissingRoles,
7422+
InvalidJsonFormat,
7423+
UnknownError
7424+
),
7425+
List(apiTagOIDC, apiTagConsumer, apiTagOAuth),
7426+
Some(List(canVerifyOidcClient))
7427+
)
7428+
7429+
lazy val verifyOidcClient: OBPEndpoint = {
7430+
case "oidc" :: "clients" :: "verify" :: Nil JsonPost json -> _ => {
7431+
cc => implicit val ec = EndpointContext(Some(cc))
7432+
for {
7433+
(Full(u), callContext) <- authenticatedAccess(cc)
7434+
_ <- if(isSuperAdmin(u.userId)) Future.successful(Full(Unit))
7435+
else NewStyle.function.hasEntitlement("", u.userId, canVerifyOidcClient, callContext)
7436+
postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the VerifyOidcClientRequestJsonV600", 400, callContext) {
7437+
json.extract[VerifyOidcClientRequestJsonV600]
7438+
}
7439+
consumerBox <- Future {
7440+
Consumers.consumers.vend.getConsumerByConsumerKey(postedData.client_id)
7441+
}
7442+
} yield {
7443+
consumerBox match {
7444+
case Full(consumer) if consumer.isActive.get && consumer.secret.get == postedData.client_secret =>
7445+
val redirectUris = Option(consumer.redirectURL.get)
7446+
.filter(_.nonEmpty)
7447+
.map(_.split("[,\\s]+").map(_.trim).filter(_.nonEmpty).toList)
7448+
(VerifyOidcClientResponseJsonV600(
7449+
valid = true,
7450+
client_id = Some(postedData.client_id),
7451+
consumer_id = Some(consumer.consumerId.get),
7452+
redirect_uris = redirectUris
7453+
), HttpCode.`200`(callContext))
7454+
case _ =>
7455+
(VerifyOidcClientResponseJsonV600(valid = false), HttpCode.`200`(callContext))
7456+
}
7457+
}
7458+
}
7459+
}
7460+
7461+
staticResourceDocs += ResourceDoc(
7462+
getOidcClient,
7463+
implementedInApiVersion,
7464+
nameOf(getOidcClient),
7465+
"GET",
7466+
"/oidc/clients/CLIENT_ID",
7467+
"Get OIDC Client",
7468+
s"""Gets an OIDC/OAuth2 client's metadata by client_id.
7469+
|
7470+
|Returns client information including name, consumer_id, redirect_uris, and enabled status.
7471+
|This endpoint does not verify the client secret - use POST /oidc/clients/verify for authentication.
7472+
|
7473+
|${userAuthenticationMessage(true)}
7474+
|""",
7475+
EmptyBody,
7476+
GetOidcClientResponseJsonV600(
7477+
client_id = "abc123def456",
7478+
client_name = "My Application",
7479+
consumer_id = "7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh",
7480+
redirect_uris = List("https://app.example.com/callback"),
7481+
enabled = true
7482+
),
7483+
List(
7484+
$AuthenticatedUserIsRequired,
7485+
UserHasMissingRoles,
7486+
UnknownError
7487+
),
7488+
List(apiTagOIDC, apiTagConsumer, apiTagOAuth),
7489+
Some(List(canGetOidcClient))
7490+
)
7491+
7492+
lazy val getOidcClient: OBPEndpoint = {
7493+
case "oidc" :: "clients" :: clientId :: Nil JsonGet _ => {
7494+
cc => implicit val ec = EndpointContext(Some(cc))
7495+
for {
7496+
(Full(u), callContext) <- authenticatedAccess(cc)
7497+
_ <- if(isSuperAdmin(u.userId)) Future.successful(Full(Unit))
7498+
else NewStyle.function.hasEntitlement("", u.userId, canGetOidcClient, callContext)
7499+
consumerBox <- Future {
7500+
Consumers.consumers.vend.getConsumerByConsumerKey(clientId)
7501+
}
7502+
consumer <- NewStyle.function.tryons(s"OBP-OIDC-003: Client not found: $clientId", 404, callContext) {
7503+
consumerBox match {
7504+
case Full(c) => c
7505+
case _ => throw new RuntimeException("Client not found")
7506+
}
7507+
}
7508+
} yield {
7509+
val redirectUris = Option(consumer.redirectURL.get)
7510+
.filter(_.nonEmpty)
7511+
.map(_.split("[,\\s]+").map(_.trim).filter(_.nonEmpty).toList)
7512+
.getOrElse(List.empty)
7513+
(GetOidcClientResponseJsonV600(
7514+
client_id = clientId,
7515+
client_name = consumer.name.get,
7516+
consumer_id = consumer.consumerId.get,
7517+
redirect_uris = redirectUris,
7518+
enabled = consumer.isActive.get
7519+
), HttpCode.`200`(callContext))
7520+
}
7521+
}
7522+
}
7523+
72247524
}
72257525
}
72267526

0 commit comments

Comments
 (0)