@@ -23,20 +23,22 @@ import code.api.v3_0_0.JSONFactory300
2323import code .api .v3_0_0 .JSONFactory300 .createAggregateMetricJson
2424import code .api .v2_0_0 .JSONFactory200
2525import 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 }
2728import code .api .v4_0_0 .JSONFactory400 .createCallsLimitJson
2829import code .api .v5_0_0 .JSONFactory500
2930import code .api .v5_0_0 .{ViewJsonV500 , ViewsJsonV500 }
3031import code .api .v5_1_0 .{JSONFactory510 , PostCustomerLegalNameJsonV510 }
3132import code .api .dynamic .entity .helper .{DynamicEntityHelper , DynamicEntityInfo }
3233import 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 }
3435import code .api .v6_0_0 .OBPAPI6_0_0
3536import code .abacrule .{AbacRuleEngine , MappedAbacRuleProvider }
3637import code .metrics .APIMetrics
3738import code .bankconnectors .{Connector , LocalMappedConnectorInternal }
3839import code .bankconnectors .storedprocedure .StoredProcedureUtils
3940import code .bankconnectors .LocalMappedConnectorInternal ._
41+ import code .consumer .Consumers
4042import code .entitlement .Entitlement
4143import code .loginattempts .LoginAttempt
4244import 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