From 37adab53785b47ffe944fca1d16e84b7f3026b3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 20:52:39 +0000 Subject: [PATCH 001/185] Bump elasticsearch from 6.8.13 to 6.8.17 in /obp-api Bumps [elasticsearch](https://github.com/elastic/elasticsearch) from 6.8.13 to 6.8.17. - [Release notes](https://github.com/elastic/elasticsearch/releases) - [Commits](https://github.com/elastic/elasticsearch/compare/v6.8.13...v6.8.17) --- updated-dependencies: - dependency-name: org.elasticsearch:elasticsearch dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- obp-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 57e6af0be..9396eb38d 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -210,7 +210,7 @@ org.elasticsearch elasticsearch - 6.8.13 + 6.8.17 com.sksamuel.elastic4s From 9b73c72b17d2b90f12ce3f225674ce63d9d379c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 13 Sep 2021 08:55:26 +0200 Subject: [PATCH 002/185] feature/Support Azure as the OpenID Connect Identity Provider 2 --- obp-api/src/main/scala/code/api/OAuth2.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 0ec4c7129..f138f5949 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -96,6 +96,8 @@ object OAuth2Login extends RestHelper with MdcLoggable { Google.applyRulesFuture(value, cc) } else if (Yahoo.isIssuer(value)) { Yahoo.applyRulesFuture(value, cc) + } else if (Azure.isIssuer(value)) { + Azure.applyRulesFuture(value, cc) } else { Hydra.applyRulesFuture(value, cc) } From 61b131bf124fa39495d6a787c6dbaeec0472e9f6 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 13 Sep 2021 11:47:14 +0200 Subject: [PATCH 003/185] bugfix/removed the .main-support-item text value fix --- obp-api/src/main/webapp/media/js/website.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/obp-api/src/main/webapp/media/js/website.js b/obp-api/src/main/webapp/media/js/website.js index 563995aca..a740571c9 100644 --- a/obp-api/src/main/webapp/media/js/website.js +++ b/obp-api/src/main/webapp/media/js/website.js @@ -120,9 +120,6 @@ $(document).ready(function() { $("#small-screen-navbar #small-nav-log-on-button").css("width","63px"); } - $(".main-support-item .support-platform-link").text("chat.openbankproject.com"); - - var htmlTitle = $(document).find("title").text(); if (htmlTitle.indexOf("Get API") > -1){ From 0160cced98353a8e6ef3e1ed05969b45f6cd1545 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 13 Sep 2021 11:52:43 +0200 Subject: [PATCH 004/185] refactor/use rocket.chat instead of slack --- obp-api/src/main/resources/props/sample.props.template | 2 +- obp-api/src/main/scala/code/snippet/WebUI.scala | 2 +- obp-api/src/main/webapp/index.html | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 2b8317b83..98b205430 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -430,7 +430,7 @@ webui_faq_url = https://openbankproject.com/faq/ webui_faq_email = contact@openbankproject.com # Link to support platform -webui_support_platform_url = https://slack.openbankproject.com/ +webui_support_platform_url = https://chat.openbankproject.com # Link to Direct Login glossary on api explorer webui_direct_login_documentation_url = diff --git a/obp-api/src/main/scala/code/snippet/WebUI.scala b/obp-api/src/main/scala/code/snippet/WebUI.scala index b5de7c2c9..64e96cd6d 100644 --- a/obp-api/src/main/scala/code/snippet/WebUI.scala +++ b/obp-api/src/main/scala/code/snippet/WebUI.scala @@ -320,7 +320,7 @@ class WebUI extends MdcLoggable{ // Support platform link def supportPlatformLink: CssSel = { - val supportplatformlink = scala.xml.Unparsed(getWebUiPropsValue("webui_support_platform_url", "https://slack.openbankproject.com/")) + val supportplatformlink = scala.xml.Unparsed(getWebUiPropsValue("webui_support_platform_url", "https://chat.openbankproject.com")) ".support-platform-link a [href]" #> supportplatformlink & ".support-platform-link a *" #> supportplatformlink.toString().replace("https://","").replace("http://", "") } diff --git a/obp-api/src/main/webapp/index.html b/obp-api/src/main/webapp/index.html index 7cda7d1dc..8cb8babfc 100644 --- a/obp-api/src/main/webapp/index.html +++ b/obp-api/src/main/webapp/index.html @@ -407,7 +407,7 @@ Berlin 13359, Germany target="_blank">FAQ, Glossary, join our Slack + class="support-platform-link" data-lift="WebUI.supportPlatformLink" href="" target="_blank">Rocket-Chat channels or email us at contact@openbankproject.com

@@ -447,9 +447,9 @@ Berlin 13359, Germany
From e5960cc9e8fe11e9837190b98e13b09f2703a906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 13 Sep 2021 11:59:17 +0200 Subject: [PATCH 005/185] feature/Add props openid_connect.show_tokens --- .../main/resources/props/sample.props.template | 1 + .../src/main/scala/code/api/openidconnect.scala | 8 +++++--- .../scala/code/model/dataAccess/AuthUser.scala | 16 +++++++++++++++- obp-api/src/main/scala/code/snippet/Login.scala | 3 ++- .../code/token/MappedOpenIDConnectToken.scala | 11 ++++++++++- .../code/token/OpenIDConnectTokenProvider.scala | 6 +++++- .../main/webapp/templates-hidden/default.html | 1 + 7 files changed, 39 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 2b8317b83..297248dd5 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -616,6 +616,7 @@ super_admin_user_ids=USER_ID1,USER_ID2, ## Note: The email address used for login must match one ## registered on OBP localy. # openid_connect.enabled=true +# openid_connect.show_tokens=false # First identity provider # openid_connect_1.button_text = Google # openid_connect_1.client_secret=OYdWujJlU7fFOW_NXzPlDI4T diff --git a/obp-api/src/main/scala/code/api/openidconnect.scala b/obp-api/src/main/scala/code/api/openidconnect.scala index 657302e01..2ed85dde2 100644 --- a/obp-api/src/main/scala/code/api/openidconnect.scala +++ b/obp-api/src/main/scala/code/api/openidconnect.scala @@ -115,7 +115,7 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { case Full(authUser) => getOrCreateConsumer(idToken, user.userId) match { case Full(consumer) => - saveAuthorizationToken(tokenType, accessToken, idToken, refreshToken, scope, expiresIn) match { + saveAuthorizationToken(tokenType, accessToken, idToken, refreshToken, scope, expiresIn, authUser.id.get) match { case Full(token) => (200, "OK", Some(authUser)) case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "1", Some(authUser)) } @@ -272,14 +272,16 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { idToken: String, refreshToken: String, scope: String, - expiresIn: Long): Box[OpenIDConnectToken] = { + expiresIn: Long, + authUserPrimaryKey: Long): Box[OpenIDConnectToken] = { val token = TokensOpenIDConnect.tokens.vend.createToken( tokenType = tokenType, accessToken = accessToken, idToken = idToken, refreshToken = refreshToken, scope = scope, - expiresIn = expiresIn + expiresIn = expiresIn, + authUserPrimaryKey = authUserPrimaryKey ) token match { case Full(_) => // All good diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index 43f140839..1a80e72f6 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -28,7 +28,7 @@ package code.model.dataAccess import code.UserRefreshes.UserRefreshes import code.accountholders.AccountHolders -import code.api.util.APIUtil.{hasAnOAuthHeader, validatePasswordOnCreation, logger, _} +import code.api.util.APIUtil.{hasAnOAuthHeader, logger, validatePasswordOnCreation, _} import code.api.util.ErrorMessages._ import code.api.util._ import code.api.v4_0_0.dynamic.DynamicEndpointHelper @@ -37,6 +37,7 @@ import code.bankconnectors.Connector import code.context.UserAuthContextProvider import code.entitlement.Entitlement import code.loginattempts.LoginAttempt +import code.token.TokensOpenIDConnect import code.users.Users import code.util.Helper import code.util.Helper.MdcLoggable @@ -486,11 +487,24 @@ import net.liftweb.util.Helpers._ getCurrentUser match { case Full(user) if user.provider.contains("google") => user.emailAddress case Full(user) if user.provider.contains("yahoo") => user.emailAddress + case Full(user) if user.provider.contains("microsoft") => user.emailAddress case Full(user) => user.name case _ => "" //TODO need more error handling for different user cases } } + def getIDTokenOfCurrentUser(): String = { + if(APIUtil.getPropsAsBoolValue("openid_connect.show_tokens", false)) { + AuthUser.currentUser match { + case Full(authUser) => + TokensOpenIDConnect.tokens.vend.getOpenIDConnectTokenByAuthUser(authUser.id.get).map(_.idToken).getOrElse("") + case _ => "" + } + } else { + "" + } + } + /** * get current user.userId * Note: 1.resourceuser has two ids: id(Long) and userid_(String), diff --git a/obp-api/src/main/scala/code/snippet/Login.scala b/obp-api/src/main/scala/code/snippet/Login.scala index ea0ff50dd..e0f58e64e 100644 --- a/obp-api/src/main/scala/code/snippet/Login.scala +++ b/obp-api/src/main/scala/code/snippet/Login.scala @@ -46,7 +46,8 @@ class Login { ".logout [href]" #> { AuthUser.logoutPath.foldLeft("")(_ + "/" + _) } & - "#loggedIn-username *" #> AuthUser.getCurrentUserUsername + "#loggedIn-username *" #> AuthUser.getCurrentUserUsername & + "#logged-in-id-token *" #> AuthUser.getIDTokenOfCurrentUser } } diff --git a/obp-api/src/main/scala/code/token/MappedOpenIDConnectToken.scala b/obp-api/src/main/scala/code/token/MappedOpenIDConnectToken.scala index cf651db72..dcf1a73eb 100644 --- a/obp-api/src/main/scala/code/token/MappedOpenIDConnectToken.scala +++ b/obp-api/src/main/scala/code/token/MappedOpenIDConnectToken.scala @@ -1,5 +1,7 @@ package code.token +import java.util.Date + import net.liftweb.common.Box import net.liftweb.mapper._ @@ -9,7 +11,8 @@ object MappedOpenIDConnectTokensProvider extends OpenIDConnectTokensProvider { idToken: String, refreshToken: String, scope: String, - expiresIn: Long): Box[OpenIDConnectToken] = Box.tryo { + expiresIn: Long, + authUserPrimaryKey: Long): Box[OpenIDConnectToken] = Box.tryo { OpenIDConnectToken.create .TokenType(tokenType.toString()) .AccessToken(accessToken) @@ -17,8 +20,12 @@ object MappedOpenIDConnectTokensProvider extends OpenIDConnectTokensProvider { .RefreshToken(refreshToken) .Scope(scope) .ExpiresIn(expiresIn) + .AuthUserPrimaryKey(authUserPrimaryKey) .saveMe() } + def getOpenIDConnectTokenByAuthUser(authUserPrimaryKey: Long) = + OpenIDConnectToken.findAll(By(OpenIDConnectToken.AuthUserPrimaryKey, authUserPrimaryKey)) + .sortBy(_.createdAt.get)(Ordering[Date].reverse).headOption } @@ -31,6 +38,7 @@ class OpenIDConnectToken extends OpenIDConnectTokenTrait with LongKeyedMapper[Op object Scope extends MappedString(this, 250) object TokenType extends MappedString(this, 250) object ExpiresIn extends MappedLong(this) + object AuthUserPrimaryKey extends MappedLong(this) override def accessToken: String = AccessToken.get override def idToken: String = IDToken.get @@ -38,6 +46,7 @@ class OpenIDConnectToken extends OpenIDConnectTokenTrait with LongKeyedMapper[Op override def scope: String = Scope.get override def tokenType: String = TokenType.get override def expiresIn: Long = ExpiresIn.get + override def authUserPrimaryKey: Long = AuthUserPrimaryKey.get } diff --git a/obp-api/src/main/scala/code/token/OpenIDConnectTokenProvider.scala b/obp-api/src/main/scala/code/token/OpenIDConnectTokenProvider.scala index efd6bffa3..8b99c7f80 100644 --- a/obp-api/src/main/scala/code/token/OpenIDConnectTokenProvider.scala +++ b/obp-api/src/main/scala/code/token/OpenIDConnectTokenProvider.scala @@ -14,7 +14,10 @@ trait OpenIDConnectTokensProvider { idToken: String, refreshToken: String, scope: String, - expiresIn: Long): Box[OpenIDConnectToken] + expiresIn: Long, + authUserPrimaryKey: Long): Box[OpenIDConnectToken] + + def getOpenIDConnectTokenByAuthUser(authUserPrimaryKey: Long): Box[OpenIDConnectToken] } trait OpenIDConnectTokenTrait { @@ -24,4 +27,5 @@ trait OpenIDConnectTokenTrait { def scope: String def tokenType: String def expiresIn: Long + def authUserPrimaryKey: Long } diff --git a/obp-api/src/main/webapp/templates-hidden/default.html b/obp-api/src/main/webapp/templates-hidden/default.html index 57103f807..36a23526d 100644 --- a/obp-api/src/main/webapp/templates-hidden/default.html +++ b/obp-api/src/main/webapp/templates-hidden/default.html @@ -133,6 +133,7 @@ Berlin 13359, Germany From ff3cf0e935e657a77c9a6e27d966eb2b1bebefc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 13 Sep 2021 15:41:49 +0200 Subject: [PATCH 006/185] feature/Add User Information Page --- .../main/scala/bootstrap/liftweb/Boot.scala | 1 + .../code/model/dataAccess/AuthUser.scala | 2 +- .../scala/code/snippet/UserInformation.scala | 66 +++++++++++++++++++ .../main/webapp/templates-hidden/default.html | 2 +- obp-api/src/main/webapp/user-information.html | 64 ++++++++++++++++++ 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 obp-api/src/main/scala/code/snippet/UserInformation.scala create mode 100644 obp-api/src/main/webapp/user-information.html diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 33830aca6..873ce5fc7 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -510,6 +510,7 @@ class Boot extends MdcLoggable { Menu("Dummy user tokens", "Get Dummy user tokens") / "dummy-user-tokens" >> AuthUser.loginFirst, Menu("Validate OTP", "Validate OTP") / "otp" >> AuthUser.loginFirst, + Menu("User Information", "User Information") / "user-information", Menu("User Invitation", "User Invitation") / "user-invitation", Menu("User Invitation Info", "User Invitation Info") / "user-invitation-info", Menu("User Invitation Invalid", "User Invitation Invalid") / "user-invitation-invalid", diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index 1a80e72f6..2bed203ff 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -501,7 +501,7 @@ import net.liftweb.util.Helpers._ case _ => "" } } else { - "" + "This information is not allowed at this instance." } } diff --git a/obp-api/src/main/scala/code/snippet/UserInformation.scala b/obp-api/src/main/scala/code/snippet/UserInformation.scala new file mode 100644 index 000000000..0bec120d0 --- /dev/null +++ b/obp-api/src/main/scala/code/snippet/UserInformation.scala @@ -0,0 +1,66 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2019, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ +package code.snippet + +import code.api.util.ErrorMessages.attemptedToOpenAnEmptyBox +import code.model.dataAccess.AuthUser +import code.util.Helper.MdcLoggable +import com.openbankproject.commons.model.User +import net.liftweb.http.{RequestVar, SHtml} +import net.liftweb.util.CssSel +import net.liftweb.util.Helpers._ + +import scala.xml.NodeSeq + +class UserInformation extends MdcLoggable { + + private object idTokenVar extends RequestVar("") + private object providerVar extends RequestVar("") + private object devEmailVar extends RequestVar("") + private object usernameVar extends RequestVar("") + + def show: CssSel = { + if(!AuthUser.loggedIn_?) { + "*" #> NodeSeq.Empty + } else if (AuthUser.getCurrentUser.isEmpty) { + "*" #> NodeSeq.Empty + } else { + val user: User = AuthUser.getCurrentUser.openOrThrowException(attemptedToOpenAnEmptyBox) + usernameVar.set(user.name) + devEmailVar.set(user.emailAddress) + providerVar.set(user.provider) + idTokenVar.set(AuthUser.getIDTokenOfCurrentUser) + "form" #> { + "#user-info-username" #> SHtml.text(usernameVar, usernameVar(_)) & + "#user-info-provider" #> SHtml.text(providerVar.is, providerVar(_)) & + "#user-info-email" #> SHtml.text(devEmailVar, devEmailVar(_)) & + "#user-info-id-token" #> SHtml.text(idTokenVar, idTokenVar(_)) + } & "#register-consumer-success" #> "" + } + } + +} diff --git a/obp-api/src/main/webapp/templates-hidden/default.html b/obp-api/src/main/webapp/templates-hidden/default.html index 36a23526d..3b6c44109 100644 --- a/obp-api/src/main/webapp/templates-hidden/default.html +++ b/obp-api/src/main/webapp/templates-hidden/default.html @@ -134,7 +134,7 @@ Berlin 13359, Germany diff --git a/obp-api/src/main/webapp/user-information.html b/obp-api/src/main/webapp/user-information.html new file mode 100644 index 000000000..d60122c6d --- /dev/null +++ b/obp-api/src/main/webapp/user-information.html @@ -0,0 +1,64 @@ + +
+
+
+

User Information

+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + From f20b0316428735cfd992b136806fb30712840152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 14 Sep 2021 07:46:06 +0200 Subject: [PATCH 007/185] refactor/Get rid of unused hidden features --- obp-api/src/main/scala/code/snippet/Login.scala | 3 +-- obp-api/src/main/webapp/templates-hidden/default.html | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/Login.scala b/obp-api/src/main/scala/code/snippet/Login.scala index e0f58e64e..ea0ff50dd 100644 --- a/obp-api/src/main/scala/code/snippet/Login.scala +++ b/obp-api/src/main/scala/code/snippet/Login.scala @@ -46,8 +46,7 @@ class Login { ".logout [href]" #> { AuthUser.logoutPath.foldLeft("")(_ + "/" + _) } & - "#loggedIn-username *" #> AuthUser.getCurrentUserUsername & - "#logged-in-id-token *" #> AuthUser.getIDTokenOfCurrentUser + "#loggedIn-username *" #> AuthUser.getCurrentUserUsername } } diff --git a/obp-api/src/main/webapp/templates-hidden/default.html b/obp-api/src/main/webapp/templates-hidden/default.html index 3b6c44109..bc4df9ae9 100644 --- a/obp-api/src/main/webapp/templates-hidden/default.html +++ b/obp-api/src/main/webapp/templates-hidden/default.html @@ -133,7 +133,6 @@ Berlin 13359, Germany From 3a537adf08577ac317fbc9d4ecb26fcf00d61fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 14 Sep 2021 08:59:11 +0200 Subject: [PATCH 008/185] feature/Add migration script to extend coulmns code and parent product code --- .../code/api/util/migration/Migration.scala | 12 ++++ .../util/migration/MigrationOfProduct.scala | 66 +++++++++++++++++++ .../products/MappedProductsProvider.scala | 4 +- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/util/migration/MigrationOfProduct.scala diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index 660a93130..e2d307817 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -84,6 +84,7 @@ object Migration extends MdcLoggable { populateTheFieldIsActiveAtProductAttribute(startedBeforeSchemifier) alterColumnUsernameProviderFirstnameAndLastnameAtAuthUser(startedBeforeSchemifier) alterColumnEmailAtResourceUser(startedBeforeSchemifier) + alterColumnCdeAndParentProductCodeAtMappedProduct(startedBeforeSchemifier) } private def dummyScript(): Boolean = { @@ -283,6 +284,17 @@ object Migration extends MdcLoggable { } } } + private def alterColumnCdeAndParentProductCodeAtMappedProduct(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.alterColumnCdeAndParentProductCodeAtMappedProduct(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(alterColumnCdeAndParentProductCodeAtMappedProduct(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfProduct.alterColumnsCodeAndPrentProductCode(name) + } + } + } } diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfProduct.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfProduct.scala new file mode 100644 index 000000000..f52816949 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfProduct.scala @@ -0,0 +1,66 @@ +package code.api.util.migration + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} + +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.products.MappedProduct +import net.liftweb.common.Full +import net.liftweb.mapper.{DB, Schemifier} +import net.liftweb.util.DefaultConnectionIdentifier + +object MigrationOfProduct { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def alterColumnsCodeAndPrentProductCode(name: String): Boolean = { + DbFunction.tableExists(MappedProduct, (DB.use(DefaultConnectionIdentifier){ conn => conn})) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { + APIUtil.getPropsValue("db.driver") match { + case Full(value) if value.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ + |ALTER TABLE mappedproduct ALTER COLUMN mcode varchar(100); + |ALTER TABLE mappedproduct ALTER COLUMN mparentproductcode varchar(100); + |""".stripMargin + case _ => + () => + """ + |ALTER TABLE mappedproduct ALTER COLUMN mcode type varchar(100); + |ALTER TABLE mappedproduct ALTER COLUMN mparentproductcode type varchar(100); + |""".stripMargin + } + + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${MappedProduct._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + +} diff --git a/obp-api/src/main/scala/code/products/MappedProductsProvider.scala b/obp-api/src/main/scala/code/products/MappedProductsProvider.scala index 4c3fa601d..e8d85d7d7 100644 --- a/obp-api/src/main/scala/code/products/MappedProductsProvider.scala +++ b/obp-api/src/main/scala/code/products/MappedProductsProvider.scala @@ -27,8 +27,8 @@ class MappedProduct extends Product with LongKeyedMapper[MappedProduct] with IdP override def getSingleton = MappedProduct object mBankId extends UUIDString(this) // combination of this - object mCode extends MappedString(this, 50) // and this is unique - object mParentProductCode extends MappedString(this, 50) // and this is unique + object mCode extends MappedString(this, 100) // and this is unique + object mParentProductCode extends MappedString(this, 100) // and this is unique object mName extends MappedString(this, 125) From a9257bd3e3d68cb30a126af10fb9ba6f1969cce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 14 Sep 2021 10:51:18 +0200 Subject: [PATCH 009/185] Revert "feature/Add migration script to extend coulmns code and parent product code" This reverts commit 3a537adf --- .../code/api/util/migration/Migration.scala | 12 ---- .../util/migration/MigrationOfProduct.scala | 66 ------------------- .../products/MappedProductsProvider.scala | 4 +- 3 files changed, 2 insertions(+), 80 deletions(-) delete mode 100644 obp-api/src/main/scala/code/api/util/migration/MigrationOfProduct.scala diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index e2d307817..660a93130 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -84,7 +84,6 @@ object Migration extends MdcLoggable { populateTheFieldIsActiveAtProductAttribute(startedBeforeSchemifier) alterColumnUsernameProviderFirstnameAndLastnameAtAuthUser(startedBeforeSchemifier) alterColumnEmailAtResourceUser(startedBeforeSchemifier) - alterColumnCdeAndParentProductCodeAtMappedProduct(startedBeforeSchemifier) } private def dummyScript(): Boolean = { @@ -284,17 +283,6 @@ object Migration extends MdcLoggable { } } } - private def alterColumnCdeAndParentProductCodeAtMappedProduct(startedBeforeSchemifier: Boolean): Boolean = { - if(startedBeforeSchemifier == true) { - logger.warn(s"Migration.database.alterColumnCdeAndParentProductCodeAtMappedProduct(true) cannot be run before Schemifier.") - true - } else { - val name = nameOf(alterColumnCdeAndParentProductCodeAtMappedProduct(startedBeforeSchemifier)) - runOnce(name) { - MigrationOfProduct.alterColumnsCodeAndPrentProductCode(name) - } - } - } } diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfProduct.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfProduct.scala deleted file mode 100644 index f52816949..000000000 --- a/obp-api/src/main/scala/code/api/util/migration/MigrationOfProduct.scala +++ /dev/null @@ -1,66 +0,0 @@ -package code.api.util.migration - -import java.time.format.DateTimeFormatter -import java.time.{ZoneId, ZonedDateTime} - -import code.api.util.APIUtil -import code.api.util.migration.Migration.{DbFunction, saveLog} -import code.products.MappedProduct -import net.liftweb.common.Full -import net.liftweb.mapper.{DB, Schemifier} -import net.liftweb.util.DefaultConnectionIdentifier - -object MigrationOfProduct { - - val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) - val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") - - def alterColumnsCodeAndPrentProductCode(name: String): Boolean = { - DbFunction.tableExists(MappedProduct, (DB.use(DefaultConnectionIdentifier){ conn => conn})) match { - case true => - val startDate = System.currentTimeMillis() - val commitId: String = APIUtil.gitCommit - var isSuccessful = false - - val executedSql = - DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { - APIUtil.getPropsValue("db.driver") match { - case Full(value) if value.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => - () => - """ - |ALTER TABLE mappedproduct ALTER COLUMN mcode varchar(100); - |ALTER TABLE mappedproduct ALTER COLUMN mparentproductcode varchar(100); - |""".stripMargin - case _ => - () => - """ - |ALTER TABLE mappedproduct ALTER COLUMN mcode type varchar(100); - |ALTER TABLE mappedproduct ALTER COLUMN mparentproductcode type varchar(100); - |""".stripMargin - } - - } - - val endDate = System.currentTimeMillis() - val comment: String = - s"""Executed SQL: - |$executedSql - |""".stripMargin - isSuccessful = true - saveLog(name, commitId, isSuccessful, startDate, endDate, comment) - isSuccessful - - case false => - val startDate = System.currentTimeMillis() - val commitId: String = APIUtil.gitCommit - val isSuccessful = false - val endDate = System.currentTimeMillis() - val comment: String = - s"""${MappedProduct._dbTableNameLC} table does not exist""".stripMargin - saveLog(name, commitId, isSuccessful, startDate, endDate, comment) - isSuccessful - } - } - -} diff --git a/obp-api/src/main/scala/code/products/MappedProductsProvider.scala b/obp-api/src/main/scala/code/products/MappedProductsProvider.scala index e8d85d7d7..4c3fa601d 100644 --- a/obp-api/src/main/scala/code/products/MappedProductsProvider.scala +++ b/obp-api/src/main/scala/code/products/MappedProductsProvider.scala @@ -27,8 +27,8 @@ class MappedProduct extends Product with LongKeyedMapper[MappedProduct] with IdP override def getSingleton = MappedProduct object mBankId extends UUIDString(this) // combination of this - object mCode extends MappedString(this, 100) // and this is unique - object mParentProductCode extends MappedString(this, 100) // and this is unique + object mCode extends MappedString(this, 50) // and this is unique + object mParentProductCode extends MappedString(this, 50) // and this is unique object mName extends MappedString(this, 125) From a86f8815d4c79209fbc3ce441ff88a6d901c1346 Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 15 Sep 2021 10:49:25 +0200 Subject: [PATCH 010/185] feature/Add migration script to extend column name to 100 --- .../code/api/util/migration/Migration.scala | 12 ++++ .../migration/MigrationOfProductFee.scala | 63 +++++++++++++++++++ .../productfee/MappedProductFeeProvider.scala | 2 +- 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 obp-api/src/main/scala/code/api/util/migration/MigrationOfProductFee.scala diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index 660a93130..c627f88b8 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -84,6 +84,7 @@ object Migration extends MdcLoggable { populateTheFieldIsActiveAtProductAttribute(startedBeforeSchemifier) alterColumnUsernameProviderFirstnameAndLastnameAtAuthUser(startedBeforeSchemifier) alterColumnEmailAtResourceUser(startedBeforeSchemifier) + alterColumnNameAtProductFee(startedBeforeSchemifier) } private def dummyScript(): Boolean = { @@ -283,6 +284,17 @@ object Migration extends MdcLoggable { } } } + private def alterColumnNameAtProductFee(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.alterColumnNameAtProductFee(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(alterColumnNameAtProductFee(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfProductFee.alterColumnProductFeeName(name) + } + } + } } diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfProductFee.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfProductFee.scala new file mode 100644 index 000000000..bbd589050 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfProductFee.scala @@ -0,0 +1,63 @@ +package code.api.util.migration + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.productfee.ProductFee +import net.liftweb.common.Full +import net.liftweb.mapper.{DB, Schemifier} +import net.liftweb.util.DefaultConnectionIdentifier + +object MigrationOfProductFee { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def alterColumnProductFeeName(name: String): Boolean = { + DbFunction.tableExists(ProductFee, (DB.use(DefaultConnectionIdentifier){ conn => conn})) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { + APIUtil.getPropsValue("db.driver") match { + case Full(value) if value.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ + |ALTER TABLE ProductFee ALTER COLUMN name varchar(100); + |""".stripMargin + case _ => + () => + """ + |ALTER TABLE ProductFee ALTER COLUMN name type varchar(100); + |""".stripMargin + } + + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${ProductFee._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + +} diff --git a/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala b/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala index 00b5f20e1..0c292eb68 100644 --- a/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala +++ b/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala @@ -95,7 +95,7 @@ class ProductFee extends ProductFeeTrait with LongKeyedMapper[ProductFee] with I object ProductFeeId extends UUIDString(this) - object Name extends MappedString(this, 50) + object Name extends MappedString(this, 100) object IsActive extends MappedBoolean(this) { override def defaultValue = true From ec6beb1788b24bed6cee733f3a5bb2315fa2367d Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 15 Sep 2021 12:15:49 +0200 Subject: [PATCH 011/185] feature/tweaked the error messages for create/update product fee --- obp-api/src/main/scala/code/api/util/ErrorMessages.scala | 2 ++ .../scala/code/productfee/MappedProductFeeProvider.scala | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 3224f8f7e..474afa523 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -372,6 +372,8 @@ object ErrorMessages { val InvalidPaymentSystemName = "OBP-30116: Invalid payment system name. The payment system name should only contain 0-9/a-z/A-Z/'-'/'.'/'_', the length should be smaller than 200." val ProductFeeNotFoundById = "OBP-30117: Product Fee not found. Please specify a valid value for PRODUCT_FEE_ID." + val CreateProductFeeError = "OBP-30118: Could not insert the Product Fee." + val UpdateProductFeeError = "OBP-30119: Could not update the Product Fee." val EntitlementIsBankRole = "OBP-30205: This entitlement is a Bank Role. Please set bank_id to a valid bank id." diff --git a/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala b/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala index 0c292eb68..8c5e01438 100644 --- a/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala +++ b/obp-api/src/main/scala/code/productfee/MappedProductFeeProvider.scala @@ -1,6 +1,7 @@ package code.productfee import code.api.util.APIUtil +import code.api.util.ErrorMessages.{CreateProductFeeError, UpdateProductFeeError} import code.util.UUIDString import com.openbankproject.commons.model.{BankId, ProductCode, ProductFeeTrait} import net.liftweb.common.{Box, Empty, Full} @@ -54,12 +55,12 @@ object MappedProductFeeProvider extends ProductFeeProvider { .Frequency(frequency) .Type(`type`) .saveMe() - } + } ?~! s"$UpdateProductFeeError" case _ => Empty } } case None => Future { - Full { + tryo { ProductFee .create .ProductFeeId(APIUtil.generateUUID) @@ -73,7 +74,7 @@ object MappedProductFeeProvider extends ProductFeeProvider { .Frequency(frequency) .Type(`type`) .saveMe() - } + } ?~! s"$CreateProductFeeError" } } } From e0b384303d48473a182c4b05021a8eb06d4e3292 Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 15 Sep 2021 17:13:54 +0200 Subject: [PATCH 012/185] docfix/added the Dynamic Endpoint and Endpoint Mapping to Glossary --- .../main/scala/code/api/util/Glossary.scala | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) 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 d1d947480..d84c42439 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2216,6 +2216,55 @@ object Glossary extends MdcLoggable { | """.stripMargin) + glossaryItems += GlossaryItem( + title = "Dynamic Endpoint", + description = + s""" +|Dynamic Endpoint, you can create dynamic endpoints by the swagger files. +|All the endpoints defined in the swagger file, will be created in OBP sandbox. +|There will be two different modes of these created endpoints. +| +|If the host of swagger is dynamic_entity, then you need link the swagger fields to the dynamic entity fields, +|please check *Endpoint Mapping* endpoints. +| +|If the host of swagger is obp_mock, every dynamic endpoint will return example response of swagger. +|If you need to link the response to external resource, please check * Method Routing* endpoints. +| +| +|Dynamic Endpoint can be created at the System level (bank_id is null) - or Bank / Space level (bank_id is not null). +|You might want to create Bank level Dynamic Entities in order to grant automated roles based on user email domain. +| +|Upon successful creation of a Dynamic Endpoint, OBP automatically: +| +|*Creates Roles to guard the above endpoints. +|*Granted yourself the entitlements to get the access to these endpoints. +| +|The following videos are available: +| +| * [Introduction to Dynamic Endpoints](https://vimeo.com/426235612) +| * [Features of Dynamic Endpoints](https://vimeo.com/444133309) +| +""".stripMargin) + + glossaryItems += GlossaryItem( + title = "Endpoint Mapping", + description = + s""" + |This can be used to map the dynamic entity fields and dynamic endpoint fields. + | + |When you create the dynamic endpoint, and set `host` of swagger to dynamic_entity. + | + |Then you can use these endpoints to map dynamic endpoint response to dynamic entity model. + | + |Check the [Create Endpoint Mapping](/index#OBPv4.0.0-createEndpointMapping) json body, you need to first know the operation_id + | + |and you can prepare the request_mapping and response_mapping objects. + | + |Details better to see the video: + | + | * [Endpoint Mapping -step1-getOne:GetAll](https://vimeo.com/553369108) + |""".stripMargin) + /////////////////////////////////////////////////////////////////// // NOTE! Some glossary items are generated in ExampleValue.scala From 3c7c5ffcf70fa4187718d1ebf97749ea63f3bdba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 15 Sep 2021 18:11:19 +0200 Subject: [PATCH 013/185] feature/Add User Agreements at User's JSON Response v4.0.0 --- .../SwaggerDefinitionsJSON.scala | 15 +++ .../main/scala/code/api/util/APIUtil.scala | 2 +- .../main/scala/code/api/util/NewStyle.scala | 5 +- .../scala/code/api/v4_0_0/APIMethods400.scala | 101 ++++++++++++++++-- .../code/api/v4_0_0/JSONFactory4.0.0.scala | 24 ++++- .../remotedata/RemotedataUserAgreement.scala | 3 + .../RemotedataUserAgreementActor.scala | 4 + .../code/remotedata/RemotedataUsers.scala | 7 +- .../remotedata/RemotedataUsersActor.scala | 4 + .../src/main/scala/code/users/LiftUsers.scala | 34 ++++-- .../main/scala/code/users/UserAgreement.scala | 7 ++ .../code/users/UserAgreementProvider.scala | 5 + obp-api/src/main/scala/code/users/Users.scala | 3 + .../test/scala/code/api/v4_0_0/UserTest.scala | 74 ++++++++++++- 14 files changed, 266 insertions(+), 22 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 5a40b3321..bd7240257 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -1701,6 +1701,18 @@ object SwaggerDefinitionsJSON { username = usernameExample.value, entitlements = entitlementJSONs ) + + val userJsonV400 = UserJsonV400( + user_id = ExampleValue.userIdExample.value, + email = ExampleValue.emailExample.value, + provider_id = providerIdValueExample.value, + provider = providerValueExample.value, + username = usernameExample.value, + entitlements = entitlementJSONs, + views = None, + agreements = Some(Nil), + is_deleted = false + ) val userIdJsonV400 = UserIdJsonV400( user_id = ExampleValue.userIdExample.value ) @@ -1965,6 +1977,9 @@ object SwaggerDefinitionsJSON { val usersJsonV200 = UsersJsonV200( users = List(userJsonV200) ) + val usersJsonV400 = UsersJsonV400( + users = List(userJsonV400) + ) val counterpartiesJSON = CounterpartiesJSON( counterparties = List(coreCounterpartyJSON) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 97487d270..ee9975643 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2989,7 +2989,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ throw new Exception(UnknownError) } } - + def unboxFullAndWrapIntoFuture[T](box: Box[T])(implicit m: Manifest[T]) : Future[T] = { Future { unboxFull(fullBoxOrException(box)) diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index f2a844196..bc5838b3a 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -36,7 +36,7 @@ import code.apicollection.{ApiCollectionTrait, MappedApiCollectionsProvider} import code.model.dataAccess.{AuthUser, BankAccountRouting} import code.standingorders.StandingOrderTrait import code.usercustomerlinks.UserCustomerLink -import code.users.{UserInvitation, UserInvitationProvider, Users} +import code.users.{UserAgreement, UserAgreementProvider, UserInvitation, UserInvitationProvider, Users} import code.util.Helper import com.openbankproject.commons.util.{ApiVersion, JsonUtils} import code.views.Views @@ -824,6 +824,9 @@ object NewStyle { connectorEmptyResponse(_, callContext) } } + def getAgreementByUserId(userId: String, agreementType: String, callContext: Option[CallContext]): Future[Box[UserAgreement]] = { + Future(UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(userId, agreementType)) + } def getEntitlementsByBankId(bankId: String, callContext: Option[CallContext]): Future[List[Entitlement]] = { Entitlement.entitlement.vend.getEntitlementsByBankId(bankId) map { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 8283d6434..c7096c497 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -41,7 +41,7 @@ import code.endpointMapping.EndpointMappingCommons import code.entitlement.Entitlement import code.metadata.counterparties.{Counterparties, MappedCounterparty} import code.metadata.tags.Tags -import code.model.dataAccess.{AuthUser, BankAccountCreation} +import code.model.dataAccess.{AuthUser, BankAccountCreation, ResourceUser} import code.model.{toUserExtended, _} import code.ratelimiting.RateLimitingDI import code.snippet.{WebUIPlaceholder, WebUITemplate} @@ -51,7 +51,7 @@ import code.transactionrequests.TransactionRequests.TransactionChallengeTypes._ import code.transactionrequests.TransactionRequests.TransactionRequestTypes import code.transactionrequests.TransactionRequests.TransactionRequestTypes.{apply => _, _} import code.userlocks.UserLocksProvider -import code.users.Users +import code.users.{UserAgreement, Users} import code.util.Helper.booleanToFuture import code.util.{Helper, JsonSchemaUtil} import code.validation.JsonValidation @@ -78,9 +78,9 @@ import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To, XHTMLMailB import net.liftweb.util.{Helpers, Mailer, StringHelpers} import org.apache.commons.collections4.CollectionUtils import org.apache.commons.lang3.StringUtils - import java.net.URLEncoder import java.util.{Calendar, Date} + import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future @@ -3384,7 +3384,7 @@ trait APIMethods400 { | """.stripMargin, EmptyBody, - usersJsonV200, + userJsonV400, List(UserNotLoggedIn, UserHasMissingRoles, UserNotFoundById, UnknownError), List(apiTagUser, apiTagNewStyle), Some(List(canGetAnyUser))) @@ -3394,14 +3394,97 @@ trait APIMethods400 { case "users" :: "user_id" :: userId :: Nil JsonGet _ => { cc => for { - (Full(u), callContext) <- authenticatedAccess(cc) - _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetAnyUser, callContext) user <- Users.users.vend.getUserByUserIdFuture(userId) map { - x => unboxFullOrFail(x, callContext, s"$UserNotFoundByUserId Current UserId($userId)") + x => unboxFullOrFail(x, cc.callContext, s"$UserNotFoundByUserId Current UserId($userId)") } - entitlements <- NewStyle.function.getEntitlementsByUserId(user.userId, callContext) + entitlements <- NewStyle.function.getEntitlementsByUserId(user.userId, cc.callContext) + acceptMarketingInfo <- NewStyle.function.getAgreementByUserId(user.userId, "accept_marketing_info", cc.callContext) + termsAndConditions <- NewStyle.function.getAgreementByUserId(user.userId, "terms_and_conditions", cc.callContext) + privacyConditions <- NewStyle.function.getAgreementByUserId(user.userId, "privacy_conditions", cc.callContext) } yield { - (JSONFactory400.createUserInfoJSON(user, entitlements), HttpCode.`200`(callContext)) + val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList + (JSONFactory400.createUserInfoJSON(user, entitlements, Some(agreements)), HttpCode.`200`(cc.callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getUserByUsername, + implementedInApiVersion, + nameOf(getUserByUsername), + "GET", + "/users/username/USERNAME", + "Get User by USERNAME", + s"""Get user by USERNAME + | + |${authenticationRequiredMessage(true)} + | + |CanGetAnyUser entitlement is required, + | + """.stripMargin, + emptyObjectJson, + userJsonV400, + List($UserNotLoggedIn, UserHasMissingRoles, UserNotFoundByUsername, UnknownError), + List(apiTagUser, apiTagNewStyle), + Some(List(canGetAnyUser))) + + + lazy val getUserByUsername: OBPEndpoint = { + case "users" :: "username" :: username :: Nil JsonGet _ => { + cc => + for { + user <- Users.users.vend.getUserByUserNameFuture(username) map { + x => unboxFullOrFail(x, cc.callContext, UserNotFoundByUsername, 404) + } + entitlements <- NewStyle.function.getEntitlementsByUserId(user.userId, cc.callContext) + acceptMarketingInfo <- NewStyle.function.getAgreementByUserId(user.userId, "accept_marketing_info", cc.callContext) + termsAndConditions <- NewStyle.function.getAgreementByUserId(user.userId, "terms_and_conditions", cc.callContext) + privacyConditions <- NewStyle.function.getAgreementByUserId(user.userId, "privacy_conditions", cc.callContext) + } yield { + val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList + (JSONFactory400.createUserInfoJSON(user, entitlements, Some(agreements)), HttpCode.`200`(cc.callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getUsers, + implementedInApiVersion, + nameOf(getUsers), + "GET", + "/users", + "Get all Users", + s"""Get all users + | + |${authenticationRequiredMessage(true)} + | + |CanGetAnyUser entitlement is required, + | + |${urlParametersDocument(false, false)} + |* locked_status (if null ignore) + | + """.stripMargin, + emptyObjectJson, + usersJsonV400, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagUser, apiTagNewStyle), + Some(List(canGetAnyUser))) + + lazy val getUsers: OBPEndpoint = { + case "users" :: Nil JsonGet _ => { + cc => + for { + httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url) + obpQueryParams <- createQueriesByHttpParamsFuture(httpParams) map { + x => unboxFullOrFail(x, cc.callContext, InvalidFilterParameterFormat) + } + users <- Users.users.vend.getUsers(obpQueryParams) + } yield { + (JSONFactory400.createUsersJson(users), HttpCode.`200`(cc.callContext)) } } } diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index a3ad2eace..05713e556 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -35,6 +35,7 @@ import code.api.util.APIUtil.{DateWithDay, DateWithSeconds, stringOptionOrNull, import code.api.v1_2_1.JSONFactory.{createAmountOfMoneyJSON, createOwnersJSON} import code.api.v1_2_1.{BankRoutingJsonV121, JSONFactory, UserJSONV121, ViewJSONV121} import code.api.v1_4_0.JSONFactory1_4_0.{LocationJsonV140, MetaJsonV140, TransactionRequestAccountJsonV140, transformToLocationFromV140, transformToMetaFromV140} +import code.api.v2_0_0.JSONFactory200.UserJsonV200 import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200, TransactionRequestChargeJsonV200} import code.api.v2_1_0.{IbanJson, JSONFactory210, PostCounterpartyBespokeJson, ResourceUserJSON} import code.api.v2_2_0.CounterpartyMetadataJson @@ -48,12 +49,13 @@ import code.atms.Atms.Atm import code.bankattribute.BankAttribute import code.consent.MappedConsent import code.entitlement.Entitlement +import code.model.dataAccess.ResourceUser import code.model.{Consumer, ModeratedBankAccount, ModeratedBankAccountCore} import code.ratelimiting.RateLimiting import code.standingorders.StandingOrderTrait import code.transactionrequests.TransactionRequests.TransactionChallengeTypes import code.userlocks.UserLocks -import code.users.UserInvitation +import code.users.{UserAgreement, UserInvitation} import com.openbankproject.commons.model.{DirectDebitTrait, ProductFeeTrait, _} import net.liftweb.common.{Box, Full} import net.liftweb.json.JValue @@ -887,6 +889,7 @@ case class AtmJsonV400 ( case class AtmsJsonV400(atms : List[AtmJsonV400]) +case class UserAgreementJson(`type`: String, text: String) case class UserJsonV400( user_id: String, email : String, @@ -895,12 +898,14 @@ case class UserJsonV400( username : String, entitlements : EntitlementJSONs, views: Option[ViewsJSON300], + agreements: Option[List[UserAgreementJson]], is_deleted: Boolean ) +case class UsersJsonV400(users: List[UserJsonV400]) object JSONFactory400 { - def createUserInfoJSON(user : User, entitlements: List[Entitlement]) : UserJsonV400 = { + def createUserInfoJSON(user : User, entitlements: List[Entitlement], agreements: Option[List[UserAgreement]]) : UserJsonV400 = { UserJsonV400( user_id = user.userId, email = user.emailAddress, @@ -909,10 +914,25 @@ object JSONFactory400 { provider = stringOrNull(user.provider), entitlements = JSONFactory200.createEntitlementJSONs(entitlements), views = None, + agreements = agreements.map(_.map( i => + UserAgreementJson(`type` = i.agreementType, text = i.agreementText)) + ), is_deleted = user.isDeleted.getOrElse(false) ) } + def createUsersJson(users : List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]) : UsersJsonV400 = { + UsersJsonV400( + users.map(t => + createUserInfoJSON( + t._1, + t._2.getOrElse(Nil), + t._3 + ) + ) + ) + } + def createCallsLimitJson(rateLimiting: RateLimiting) : CallLimitJsonV400 = { CallLimitJsonV400( rateLimiting.fromDate, diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreement.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreement.scala index 9960f49a7..bc872e22c 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreement.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreement.scala @@ -16,4 +16,7 @@ object RemotedataUserAgreement extends ObpActorInit with UserAgreementProvider { def createUserAgreement(userId: String, agreementType: String, agreementText: String): Box[UserAgreement] = getValueFromFuture( (actor ? cc.createOrUpdateUserAgreement(userId, agreementType, agreementText)).mapTo[Box[UserAgreement]] ) + def getUserAgreement(userId: String, agreementType: String): Box[UserAgreement] = getValueFromFuture( + (actor ? cc.getUserAgreement(userId, agreementType)).mapTo[Box[UserAgreement]] + ) } diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreementActor.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreementActor.scala index cac3ddcb7..0967f7670 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreementActor.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUserAgreementActor.scala @@ -20,6 +20,10 @@ class RemotedataUserAgreementActor extends Actor with ObpActorHelper with MdcLog logger.debug(s"createUserAgreement($userId, $agreementType, $agreementText)") sender ! (mapper.createUserAgreement(userId, agreementType, agreementText)) + case cc.getUserAgreement(userId: String, agreementType: String) => + logger.debug(s"getUserAgreement($userId, $agreementType)") + sender ! (mapper.getUserAgreement(userId, agreementType)) + case message => logger.warn("[AKKA ACTOR ERROR - REQUEST NOT RECOGNIZED] " + message) } diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala index faa3b2a6c..596de4772 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala @@ -5,7 +5,7 @@ import code.actorsystem.ObpActorInit import code.api.util.OBPQueryParam import code.entitlement.Entitlement import code.model.dataAccess.ResourceUser -import code.users.{RemotedataUsersCaseClasses, Users} +import code.users.{RemotedataUsersCaseClasses, UserAgreement, Users} import com.openbankproject.commons.model.{User, UserPrimaryKey} import net.liftweb.common.Box @@ -70,6 +70,11 @@ object RemotedataUsers extends ObpActorInit with Users { val res = (actor ? cc.getAllUsersF(queryParams)) res.mapTo[List[(ResourceUser, Box[List[Entitlement]])]] } + + def getUsers(queryParams: List[OBPQueryParam]): Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] = { + val res = (actor ? cc.getUsers(queryParams)) + res.mapTo[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] + } def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) : Box[ResourceUser] = getValueFromFuture( (actor ? cc.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, createdByUserInvitationId, company)).mapTo[Box[ResourceUser]] diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala index a088959ed..eab4b368c 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala @@ -78,6 +78,10 @@ class RemotedataUsersActor extends Actor with ObpActorHelper with MdcLoggable { case cc.getAllUsersF(queryParams: List[OBPQueryParam]) => logger.debug(s"getAllUsersF(queryParams: ($queryParams))") mapper.getAllUsersF(queryParams) pipeTo sender + + case cc.getUsers(queryParams: List[OBPQueryParam]) => + logger.debug(s"getUsers(queryParams: ($queryParams))") + mapper.getUsers(queryParams) pipeTo sender case cc.createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) => logger.debug("createResourceUser(" + provider + ", " + providerId.getOrElse("None") + ", " + name.getOrElse("None") + ", " + email.getOrElse("None") + ", " + userId.getOrElse("None") + ", " + createdByUserInvitationId.getOrElse("None") + ", " + company.getOrElse("None") + ")") diff --git a/obp-api/src/main/scala/code/users/LiftUsers.scala b/obp-api/src/main/scala/code/users/LiftUsers.scala index 99c1168e0..4071da280 100644 --- a/obp-api/src/main/scala/code/users/LiftUsers.scala +++ b/obp-api/src/main/scala/code/users/LiftUsers.scala @@ -123,11 +123,21 @@ object LiftUsers extends Users with MdcLoggable{ } override def getAllUsersF(queryParams: List[OBPQueryParam]): Future[List[(ResourceUser, Box[List[Entitlement]])]] = { - + Future { + for { + user <- getUsersCommon(queryParams) + } yield { + (user, Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName))) + } + } + } + + + private def getUsersCommon(queryParams: List[OBPQueryParam]) = { val limit = queryParams.collect { case OBPLimit(value) => MaxRows[ResourceUser](value) }.headOption val offset: Option[StartAt[ResourceUser]] = queryParams.collect { case OBPOffset(value) => StartAt[ResourceUser](value) }.headOption val locked: Option[String] = queryParams.collect { case OBPLockedStatus(value) => value }.headOption - val deleted = queryParams.collect { + val deleted = queryParams.collect { case OBPIsDeleted(value) if value == true => // ?is_deleted=true By(ResourceUser.IsDeleted, true) case OBPIsDeleted(value) if value == false => // ?is_deleted=false @@ -135,13 +145,14 @@ object LiftUsers extends Users with MdcLoggable{ }.headOption.orElse( Some(By(ResourceUser.IsDeleted, false)) // There is no query parameter "is_deleted" ) - + val optionalParams: Seq[QueryParam[ResourceUser]] = Seq(limit.toSeq, offset.toSeq, deleted.toSeq).flatten - + def getAllResourceUsers(): List[ResourceUser] = ResourceUser.findAll(optionalParams: _*) + val showUsers: List[ResourceUser] = locked.map(_.toLowerCase()) match { case Some("active") => - val lockedUsers: immutable.Seq[MappedBadLoginAttempt] = + val lockedUsers: immutable.Seq[MappedBadLoginAttempt] = MappedBadLoginAttempt.findAll( By_>(MappedBadLoginAttempt.mBadAttemptsSinceLastSuccessOrReset, maxBadLoginAttempts.toInt) ) @@ -157,11 +168,20 @@ object LiftUsers extends Users with MdcLoggable{ case _ => getAllResourceUsers() } + showUsers + } + + override def getUsers(queryParams: List[OBPQueryParam]): Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] = { Future { for { - user <- showUsers + user <- getUsersCommon(queryParams) } yield { - (user, Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName))) + val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName)) + val acceptMarketingInfo = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "accept_marketing_info") + val termsAndConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "terms_and_conditions") + val privacyConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "privacy_conditions") + val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList + (user, entitlements, Some(agreements)) } } } diff --git a/obp-api/src/main/scala/code/users/UserAgreement.scala b/obp-api/src/main/scala/code/users/UserAgreement.scala index 9dc3439bd..19a643279 100644 --- a/obp-api/src/main/scala/code/users/UserAgreement.scala +++ b/obp-api/src/main/scala/code/users/UserAgreement.scala @@ -43,6 +43,12 @@ object MappedUserAgreementProvider extends UserAgreementProvider { .saveMe() ) } + override def getUserAgreement(userId: String, agreementType: String): Box[UserAgreement] = { + UserAgreement.findAll( + By(UserAgreement.UserId, userId), + By(UserAgreement.AgreementType, agreementType) + ).sortBy(_.Date.get)(Ordering[Date].reverse).headOption + } } class UserAgreement extends UserAgreementTrait with LongKeyedMapper[UserAgreement] with IdPK with CreatedUpdated { @@ -64,6 +70,7 @@ class UserAgreement extends UserAgreementTrait with LongKeyedMapper[UserAgreemen override def agreementType: String = AgreementType.get override def agreementText: String = AgreementText.get override def agreementHash: String = AgreementHash.get + override def date: Date = Date.get } object UserAgreement extends UserAgreement with LongKeyedMetaMapper[UserAgreement] { diff --git a/obp-api/src/main/scala/code/users/UserAgreementProvider.scala b/obp-api/src/main/scala/code/users/UserAgreementProvider.scala index 85d57dbb2..30c9a049e 100644 --- a/obp-api/src/main/scala/code/users/UserAgreementProvider.scala +++ b/obp-api/src/main/scala/code/users/UserAgreementProvider.scala @@ -1,5 +1,7 @@ package code.users +import java.util.Date + import code.api.util.APIUtil import code.remotedata.RemotedataUserAgreement import net.liftweb.common.Box @@ -21,11 +23,13 @@ object UserAgreementProvider extends SimpleInjector { trait UserAgreementProvider { def createOrUpdateUserAgreement(userId: String, agreementType: String, agreementText: String): Box[UserAgreement] def createUserAgreement(userId: String, agreementType: String, agreementText: String): Box[UserAgreement] + def getUserAgreement(userId: String, agreementType: String): Box[UserAgreement] } class RemotedataUserAgreementProviderCaseClass { case class createOrUpdateUserAgreement(userId: String, agreementType: String, agreementText: String) case class createUserAgreement(userId: String, agreementType: String, agreementText: String) + case class getUserAgreement(userId: String, agreementType: String) } object RemotedataUserAgreementProviderCaseClass extends RemotedataUserAgreementProviderCaseClass @@ -36,4 +40,5 @@ trait UserAgreementTrait { def agreementType: String def agreementText: String def agreementHash: String + def date: Date } \ No newline at end of file diff --git a/obp-api/src/main/scala/code/users/Users.scala b/obp-api/src/main/scala/code/users/Users.scala index 771d37a78..a84512374 100644 --- a/obp-api/src/main/scala/code/users/Users.scala +++ b/obp-api/src/main/scala/code/users/Users.scala @@ -51,6 +51,8 @@ trait Users { def getAllUsersF(queryParams: List[OBPQueryParam]) : Future[List[(ResourceUser, Box[List[Entitlement]])]] + def getUsers(queryParams: List[OBPQueryParam]): Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] + def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) : Box[ResourceUser] def createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) : Box[ResourceUser] @@ -80,6 +82,7 @@ class RemotedataUsersCaseClasses { case class getUserByEmailFuture(email : String) case class getAllUsers() case class getAllUsersF(queryParams: List[OBPQueryParam]) + case class getUsers(queryParams: List[OBPQueryParam]) case class createResourceUser(provider: String, providerId: Option[String],createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) case class createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) case class saveResourceUser(resourceUser: ResourceUser) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala index adbc5274d..10734a17b 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala @@ -1,10 +1,14 @@ package code.api.v4_0_0 +import java.util.UUID + import code.api.util.APIUtil.OAuth._ import code.api.util.ApiRole.CanGetAnyUser -import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} +import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn, attemptedToOpenAnEmptyBox} import code.api.v4_0_0.OBPAPI4_0_0.Implementations4_0_0 import code.entitlement.Entitlement +import code.model.UserX +import code.users.Users import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.ApiVersion @@ -21,6 +25,8 @@ class UserTest extends V400ServerSetup { object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.getCurrentUserId)) object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getUserByUserId)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getUsers)) + object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.getUserByUsername)) feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") { @@ -76,6 +82,72 @@ class UserTest extends V400ServerSetup { response400.body.extract[UserJsonV400].user_id should equal(resourceUser3.userId) } } + + feature(s"test $ApiEndpoint3 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint3, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users").GET + val response400 = makeGetRequest(request400) + Then("We should get a 401") + response400.code should equal(401) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + feature(s"test $ApiEndpoint3 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials but without a proper entitlement", ApiEndpoint3, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("error should be " + UserHasMissingRoles + CanGetAnyUser) + response400.code should equal(403) + response400.body.extract[ErrorMessage].message should be (UserHasMissingRoles + CanGetAnyUser) + } + } + feature(s"test $ApiEndpoint3 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials and a proper entitlement", ApiEndpoint3, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString) + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("We get successful response") + response400.code should equal(200) + response400.body.extract[UsersJsonV400] + } + } + + feature(s"test $ApiEndpoint4 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "username" / "USERNAME").GET + val response400 = makeGetRequest(request400) + Then("We should get a 401") + response400.code should equal(401) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + feature(s"test $ApiEndpoint4 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials but without a proper entitlement", ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "username" / "USERNAME").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("error should be " + UserHasMissingRoles + CanGetAnyUser) + response400.code should equal(403) + response400.body.extract[ErrorMessage].message should be (UserHasMissingRoles + CanGetAnyUser) + } + } + feature(s"test $ApiEndpoint4 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials and a proper entitlement", ApiEndpoint4, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString) + val user = UserX.createResourceUser(defaultProvider, Some("user.name.1"), None, Some("user.name.1"), None, Some(UUID.randomUUID.toString), None).openOrThrowException(attemptedToOpenAnEmptyBox) + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "username" / user.idGivenByProvider).GET <@(user1) + val response400 = makeGetRequest(request400) + Then("We get successful response") + response400.code should equal(200) + response400.body.extract[UserJsonV400] + Users.users.vend.deleteResourceUser(user.id.get) + } + } } From 536ea172966c1a939266f362ef4fc18e99395a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 15 Sep 2021 18:58:34 +0200 Subject: [PATCH 014/185] feature/Add endpoint getUsersByEmail v4.0.0 --- .../scala/code/api/v4_0_0/APIMethods400.scala | 32 +++++++++++++++++ .../code/remotedata/RemotedataUsers.scala | 3 ++ .../remotedata/RemotedataUsersActor.scala | 4 +++ .../src/main/scala/code/users/LiftUsers.scala | 14 ++++++++ obp-api/src/main/scala/code/users/Users.scala | 2 ++ .../test/scala/code/api/v4_0_0/UserTest.scala | 35 +++++++++++++++++++ 6 files changed, 90 insertions(+) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index c7096c497..3e1136a53 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -3447,6 +3447,38 @@ trait APIMethods400 { } } + + staticResourceDocs += ResourceDoc( + getUsersByEmail, + implementedInApiVersion, + nameOf(getUsersByEmail), + "GET", + "/users/email/EMAIL/terminator", + "Get Users by Email Address", + s"""Get users by email address + | + |${authenticationRequiredMessage(true)} + |CanGetAnyUser entitlement is required, + | + """.stripMargin, + emptyObjectJson, + usersJsonV400, + List($UserNotLoggedIn, UserHasMissingRoles, UserNotFoundByEmail, UnknownError), + List(apiTagUser, apiTagNewStyle), + Some(List(canGetAnyUser))) + + + lazy val getUsersByEmail: OBPEndpoint = { + case "users" :: "email" :: email :: "terminator" :: Nil JsonGet _ => { + cc => + for { + users <- Users.users.vend.getUsersByEmail(email) + } yield { + (JSONFactory400.createUsersJson(users), HttpCode.`200`(cc.callContext)) + } + } + } + staticResourceDocs += ResourceDoc( getUsers, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala index 596de4772..50c9eef34 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala @@ -61,6 +61,9 @@ object RemotedataUsers extends ObpActorInit with Users { def getUserByEmailFuture(email : String) : Future[List[(ResourceUser, Box[List[Entitlement]])]] = (actor ? cc.getUserByEmailFuture(email)).mapTo[List[(ResourceUser, Box[List[Entitlement]])]] + + def getUsersByEmail(email : String) : Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] = + (actor ? cc.getUsersByEmail(email)).mapTo[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] def getAllUsers() : Box[List[ResourceUser]] = getValueFromFuture( (actor ? cc.getAllUsers()).mapTo[Box[List[ResourceUser]]] diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala index eab4b368c..696999375 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala @@ -70,6 +70,10 @@ class RemotedataUsersActor extends Actor with ObpActorHelper with MdcLoggable { case cc.getUserByEmailFuture(email: String) => logger.debug("getUserByEmailFuture(" + email +")") sender ! (mapper.getUserByEmailF(email)) + + case cc.getUsersByEmail(email: String) => + logger.debug("getUsersByEmail(" + email +")") + mapper.getUsersByEmail(email) pipeTo sender case cc.getAllUsers() => logger.debug("getAllUsers()") diff --git a/obp-api/src/main/scala/code/users/LiftUsers.scala b/obp-api/src/main/scala/code/users/LiftUsers.scala index 4071da280..f9e610510 100644 --- a/obp-api/src/main/scala/code/users/LiftUsers.scala +++ b/obp-api/src/main/scala/code/users/LiftUsers.scala @@ -111,6 +111,20 @@ object LiftUsers extends Users with MdcLoggable{ (user, Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName))) } } + + override def getUsersByEmail(email: String): Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] = Future { + val users = ResourceUser.findAll(By(ResourceUser.email, email)) + for { + user <- users + } yield { + val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName)) + val acceptMarketingInfo = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "accept_marketing_info") + val termsAndConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "terms_and_conditions") + val privacyConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "privacy_conditions") + val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList + (user, entitlements, Some(agreements)) + } + } override def getUserByEmailFuture(email: String): Future[List[(ResourceUser, Box[List[Entitlement]])]] = { Future { diff --git a/obp-api/src/main/scala/code/users/Users.scala b/obp-api/src/main/scala/code/users/Users.scala index a84512374..ef6344eb5 100644 --- a/obp-api/src/main/scala/code/users/Users.scala +++ b/obp-api/src/main/scala/code/users/Users.scala @@ -46,6 +46,7 @@ trait Users { def getUserByEmail(email: String) : Box[List[ResourceUser]] def getUserByEmailFuture(email: String) : Future[List[(ResourceUser, Box[List[Entitlement]])]] + def getUsersByEmail(email: String) : Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] def getAllUsers() : Box[List[ResourceUser]] @@ -80,6 +81,7 @@ class RemotedataUsersCaseClasses { case class getUserByUserNameFuture(userName : String) case class getUserByEmail(email : String) case class getUserByEmailFuture(email : String) + case class getUsersByEmail(email : String) case class getAllUsers() case class getAllUsersF(queryParams: List[OBPQueryParam]) case class getUsers(queryParams: List[OBPQueryParam]) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala index 10734a17b..614563610 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/UserTest.scala @@ -27,6 +27,7 @@ class UserTest extends V400ServerSetup { object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getUserByUserId)) object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getUsers)) object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.getUserByUsername)) + object ApiEndpoint5 extends Tag(nameOf(Implementations4_0_0.getUsersByEmail)) feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") { @@ -149,5 +150,39 @@ class UserTest extends V400ServerSetup { } } + feature(s"test $ApiEndpoint5 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint5, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "email" / "EMAIL" / "terminator").GET + val response400 = makeGetRequest(request400) + Then("We should get a 401") + response400.code should equal(401) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + feature(s"test $ApiEndpoint5 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials but without a proper entitlement", ApiEndpoint5, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "email" / "EMAIL" / "terminator").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("error should be " + UserHasMissingRoles + CanGetAnyUser) + response400.code should equal(403) + response400.body.extract[ErrorMessage].message should be (UserHasMissingRoles + CanGetAnyUser) + } + } + feature(s"test $ApiEndpoint5 version $VersionOfApi - Authorized access") { + scenario("We will call the endpoint with user credentials and a proper entitlement", ApiEndpoint5, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString) + val user = UserX.createResourceUser(defaultProvider, Some("user.name.1"), None, Some("user.name.1"), Some("test@tesobe.com"), Some(UUID.randomUUID.toString), None).openOrThrowException(attemptedToOpenAnEmptyBox) + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "users" / "email" / user.emailAddress / "terminator").GET <@(user1) + val response400 = makeGetRequest(request400) + Then("We get successful response") + response400.code should equal(200) + response400.body.extract[UsersJsonV400] + Users.users.vend.deleteResourceUser(user.id.get) + } + } + } From 93937bac0a246dc3e34d9ddf94519c41224b6208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 16 Sep 2021 09:32:12 +0200 Subject: [PATCH 015/185] refactor/Add User Agreements at User's JSON Response v4.0.0 --- .../src/main/scala/code/users/LiftUsers.scala | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/scala/code/users/LiftUsers.scala b/obp-api/src/main/scala/code/users/LiftUsers.scala index f9e610510..56a68a496 100644 --- a/obp-api/src/main/scala/code/users/LiftUsers.scala +++ b/obp-api/src/main/scala/code/users/LiftUsers.scala @@ -118,14 +118,19 @@ object LiftUsers extends Users with MdcLoggable{ user <- users } yield { val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName)) - val acceptMarketingInfo = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "accept_marketing_info") - val termsAndConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "terms_and_conditions") - val privacyConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "privacy_conditions") - val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList + val agreements = getUserAgreements(user) (user, entitlements, Some(agreements)) } } + private def getUserAgreements(user: ResourceUser) = { + val acceptMarketingInfo = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "accept_marketing_info") + val termsAndConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "terms_and_conditions") + val privacyConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "privacy_conditions") + val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList + agreements + } + override def getUserByEmailFuture(email: String): Future[List[(ResourceUser, Box[List[Entitlement]])]] = { Future { getUserByEmailF(email) @@ -191,10 +196,7 @@ object LiftUsers extends Users with MdcLoggable{ user <- getUsersCommon(queryParams) } yield { val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName)) - val acceptMarketingInfo = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "accept_marketing_info") - val termsAndConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "terms_and_conditions") - val privacyConditions = UserAgreementProvider.userAgreementProvider.vend.getUserAgreement(user.userId, "privacy_conditions") - val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList + val agreements = getUserAgreements(user) (user, entitlements, Some(agreements)) } } From 138af5eb85231fc2f75e1c8a180bc3a51df30f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 16 Sep 2021 12:39:49 +0200 Subject: [PATCH 016/185] bugfix/Enforce Auth User validations at user invitation flow --- .../scala/code/snippet/UserInvitation.scala | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/UserInvitation.scala b/obp-api/src/main/scala/code/snippet/UserInvitation.scala index 94b18e002..c4c38f61f 100644 --- a/obp-api/src/main/scala/code/snippet/UserInvitation.scala +++ b/obp-api/src/main/scala/code/snippet/UserInvitation.scala @@ -36,7 +36,7 @@ import code.util.Helper import code.util.Helper.MdcLoggable import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue import com.openbankproject.commons.model.User -import net.liftweb.common.{Box, Full} +import net.liftweb.common.{Box, Empty, Failure, Full} import net.liftweb.http.{RequestVar, S, SHtml} import net.liftweb.util.CssSel import net.liftweb.util.Helpers._ @@ -96,25 +96,30 @@ class UserInvitation extends MdcLoggable { createResourceUser( provider = "OBP-User-Invitation", providerId = Some(usernameVar.is), - name = Some(firstNameVar.is + " " + lastNameVar.is), + name = Some(usernameVar.is), email = Some(email), userInvitationId = userInvitation.map(_.userInvitationId).toOption, company = userInvitation.map(_.company).toOption ).map{ u => // AuthUser table - createAuthUser(user = u, firstName = firstNameVar.is, lastName = lastNameVar.is, password = "") - // Use Agreement table - UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( - u.userId, "privacy_conditions", privacyConditionsValue) - UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( - u.userId, "terms_and_conditions", termsAndConditionsValue) - UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( - u.userId, "accept_marketing_info", marketingInfoCheckboxVar.is.toString) - // Set the status of the user invitation to "FINISHED" - UserInvitationProvider.userInvitationProvider.vend.updateStatusOfUserInvitation(userInvitation.map(_.userInvitationId).getOrElse(""), "FINISHED") - // Set a new password - val resetLink = AuthUser.passwordResetUrl(u.idGivenByProvider, u.emailAddress, u.userId) + "?action=set" - S.redirectTo(resetLink) + createAuthUser(user = u, firstName = firstNameVar.is, lastName = lastNameVar.is, password = "") match { + case Failure(msg,_,_) => + Users.users.vend.deleteResourceUser(u.id.get) + showError(msg) + case _ => + // User Agreement table + UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( + u.userId, "privacy_conditions", privacyConditionsValue) + UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( + u.userId, "terms_and_conditions", termsAndConditionsValue) + UserAgreementProvider.userAgreementProvider.vend.createOrUpdateUserAgreement( + u.userId, "accept_marketing_info", marketingInfoCheckboxVar.is.toString) + // Set the status of the user invitation to "FINISHED" + UserInvitationProvider.userInvitationProvider.vend.updateStatusOfUserInvitation(userInvitation.map(_.userInvitationId).getOrElse(""), "FINISHED") + // Set a new password + val resetLink = AuthUser.passwordResetUrl(u.idGivenByProvider, u.emailAddress, u.userId) + "?action=set" + S.redirectTo(resetLink) + } } } @@ -181,18 +186,24 @@ class UserInvitation extends MdcLoggable { register } - private def createAuthUser(user: User, firstName: String, lastName: String, password: String): Box[AuthUser] = tryo { + private def createAuthUser(user: User, firstName: String, lastName: String, password: String): Box[AuthUser] = { val newUser = AuthUser.create .firstName(firstName) .lastName(lastName) .email(user.emailAddress) .user(user.userPrimaryKey.value) - .username(user.idGivenByProvider) + .username(user.name) .provider(user.provider) .password(password) .validated(true) - // Save the user - newUser.saveMe() + newUser.validate match { + case Nil => + // Save the user + Full(newUser.saveMe()) + case xs => S.error(xs) + Failure(xs.map(i => i.msg).mkString(";")) + } + } private def createResourceUser(provider: String, From 39b83a8e7a06616a6076ce7407641e0585a17f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 16 Sep 2021 13:31:56 +0200 Subject: [PATCH 017/185] feature/Remove User Agreements at User's JSON Response v4.0.0 except getUserByUserId endpoint --- .../ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala | 11 +++++++++++ .../main/scala/code/api/v4_0_0/APIMethods400.scala | 8 ++------ obp-api/src/main/scala/code/users/LiftUsers.scala | 8 ++++---- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index bd7240257..60e866dad 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -1703,6 +1703,17 @@ object SwaggerDefinitionsJSON { ) val userJsonV400 = UserJsonV400( + user_id = ExampleValue.userIdExample.value, + email = ExampleValue.emailExample.value, + provider_id = providerIdValueExample.value, + provider = providerValueExample.value, + username = usernameExample.value, + entitlements = entitlementJSONs, + views = None, + agreements = None, + is_deleted = false + ) + val userJsonWithAgreementsV400 = UserJsonV400( user_id = ExampleValue.userIdExample.value, email = ExampleValue.emailExample.value, provider_id = providerIdValueExample.value, diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 3e1136a53..42e71be4b 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -3384,7 +3384,7 @@ trait APIMethods400 { | """.stripMargin, EmptyBody, - userJsonV400, + userJsonWithAgreementsV400, List(UserNotLoggedIn, UserHasMissingRoles, UserNotFoundById, UnknownError), List(apiTagUser, apiTagNewStyle), Some(List(canGetAnyUser))) @@ -3437,12 +3437,8 @@ trait APIMethods400 { x => unboxFullOrFail(x, cc.callContext, UserNotFoundByUsername, 404) } entitlements <- NewStyle.function.getEntitlementsByUserId(user.userId, cc.callContext) - acceptMarketingInfo <- NewStyle.function.getAgreementByUserId(user.userId, "accept_marketing_info", cc.callContext) - termsAndConditions <- NewStyle.function.getAgreementByUserId(user.userId, "terms_and_conditions", cc.callContext) - privacyConditions <- NewStyle.function.getAgreementByUserId(user.userId, "privacy_conditions", cc.callContext) } yield { - val agreements = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList - (JSONFactory400.createUserInfoJSON(user, entitlements, Some(agreements)), HttpCode.`200`(cc.callContext)) + (JSONFactory400.createUserInfoJSON(user, entitlements, None), HttpCode.`200`(cc.callContext)) } } } diff --git a/obp-api/src/main/scala/code/users/LiftUsers.scala b/obp-api/src/main/scala/code/users/LiftUsers.scala index 56a68a496..e8dcf9f45 100644 --- a/obp-api/src/main/scala/code/users/LiftUsers.scala +++ b/obp-api/src/main/scala/code/users/LiftUsers.scala @@ -118,8 +118,8 @@ object LiftUsers extends Users with MdcLoggable{ user <- users } yield { val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName)) - val agreements = getUserAgreements(user) - (user, entitlements, Some(agreements)) + // val agreements = getUserAgreements(user) + (user, entitlements, None) } } @@ -196,8 +196,8 @@ object LiftUsers extends Users with MdcLoggable{ user <- getUsersCommon(queryParams) } yield { val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).map(_.sortWith(_.roleName < _.roleName)) - val agreements = getUserAgreements(user) - (user, entitlements, Some(agreements)) + // val agreements = getUserAgreements(user) + (user, entitlements, None) } } } From 1156efa4d97bbf94765fb6b8fa9421464a03b1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 16 Sep 2021 16:35:38 +0200 Subject: [PATCH 018/185] bugfix/Enforce Auth User validations at user invitation flow 2 --- .../main/scala/code/api/util/SecureRandomUtil.scala | 5 +++++ .../src/main/scala/code/snippet/UserInvitation.scala | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/SecureRandomUtil.scala b/obp-api/src/main/scala/code/api/util/SecureRandomUtil.scala index 095cc9c93..a11b8c2e1 100644 --- a/obp-api/src/main/scala/code/api/util/SecureRandomUtil.scala +++ b/obp-api/src/main/scala/code/api/util/SecureRandomUtil.scala @@ -1,5 +1,6 @@ package code.api.util +import java.math.BigInteger import java.security.SecureRandom /** @@ -15,4 +16,8 @@ object SecureRandomUtil { // Obtains random numbers from the underlying native OS. // No assertions are made as to the blocking nature of generating these numbers. val csprng = SecureRandom.getInstance("NativePRNG") + + def alphanumeric(nrChars: Int = 24): String = { + new BigInteger(nrChars * 5, csprng).toString(32) + } } diff --git a/obp-api/src/main/scala/code/snippet/UserInvitation.scala b/obp-api/src/main/scala/code/snippet/UserInvitation.scala index c4c38f61f..9785fb22a 100644 --- a/obp-api/src/main/scala/code/snippet/UserInvitation.scala +++ b/obp-api/src/main/scala/code/snippet/UserInvitation.scala @@ -28,7 +28,7 @@ package code.snippet import java.time.{Duration, ZoneId, ZoneOffset, ZonedDateTime} -import code.api.util.APIUtil +import code.api.util.{APIUtil, SecureRandomUtil} import code.model.dataAccess.{AuthUser, ResourceUser} import code.users import code.users.{UserAgreementProvider, UserInvitationProvider, Users} @@ -38,7 +38,7 @@ import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue import com.openbankproject.commons.model.User import net.liftweb.common.{Box, Empty, Failure, Full} import net.liftweb.http.{RequestVar, S, SHtml} -import net.liftweb.util.CssSel +import net.liftweb.util.{CssSel, Helpers} import net.liftweb.util.Helpers._ import scala.collection.immutable.List @@ -102,7 +102,7 @@ class UserInvitation extends MdcLoggable { company = userInvitation.map(_.company).toOption ).map{ u => // AuthUser table - createAuthUser(user = u, firstName = firstNameVar.is, lastName = lastNameVar.is, password = "") match { + createAuthUser(user = u, firstName = firstNameVar.is, lastName = lastNameVar.is) match { case Failure(msg,_,_) => Users.users.vend.deleteResourceUser(u.id.get) showError(msg) @@ -186,7 +186,7 @@ class UserInvitation extends MdcLoggable { register } - private def createAuthUser(user: User, firstName: String, lastName: String, password: String): Box[AuthUser] = { + private def createAuthUser(user: User, firstName: String, lastName: String): Box[AuthUser] = { val newUser = AuthUser.create .firstName(firstName) .lastName(lastName) @@ -194,7 +194,7 @@ class UserInvitation extends MdcLoggable { .user(user.userPrimaryKey.value) .username(user.name) .provider(user.provider) - .password(password) + .password(SecureRandomUtil.alphanumeric(10)) .validated(true) newUser.validate match { case Nil => From a265d5152859e96f2e385888f3f0020cefc52bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 16 Sep 2021 17:05:27 +0200 Subject: [PATCH 019/185] refactor/Add field last_marketing_agreement_signed_date to resourceuser table --- obp-api/src/main/scala/code/api/GatewayLogin.scala | 3 ++- obp-api/src/main/scala/code/api/OAuth2.scala | 3 ++- .../src/main/scala/code/api/openidconnect.scala | 3 ++- .../src/main/scala/code/api/util/ConsentUtil.scala | 3 ++- .../src/main/scala/code/model/BankingData.scala | 1 + obp-api/src/main/scala/code/model/User.scala | 2 +- .../scala/code/model/dataAccess/ResourceUser.scala | 4 ++++ .../scala/code/remotedata/RemotedataUsers.scala | 6 ++++-- .../code/remotedata/RemotedataUsersActor.scala | 8 +++++--- .../main/scala/code/snippet/UserInvitation.scala | 10 +++++++--- obp-api/src/main/scala/code/users/LiftUsers.scala | 12 ++++++++++-- obp-api/src/main/scala/code/users/Users.scala | 14 ++++++++++++-- .../BankAccountCreationListenerTest.scala | 2 +- .../commons/model/CommonModel.scala | 11 ++++++++++- .../openbankproject/commons/model/UserModel.scala | 3 +++ 15 files changed, 66 insertions(+), 19 deletions(-) diff --git a/obp-api/src/main/scala/code/api/GatewayLogin.scala b/obp-api/src/main/scala/code/api/GatewayLogin.scala index aaf83020e..9d2128a18 100755 --- a/obp-api/src/main/scala/code/api/GatewayLogin.scala +++ b/obp-api/src/main/scala/code/api/GatewayLogin.scala @@ -262,7 +262,8 @@ object GatewayLogin extends RestHelper with MdcLoggable { email = None, userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) } match { case Full(u) => diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index f138f5949..e77309bac 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -287,7 +287,8 @@ object OAuth2Login extends RestHelper with MdcLoggable { email = getClaim(name = "email", idToken = idToken), userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) } } diff --git a/obp-api/src/main/scala/code/api/openidconnect.scala b/obp-api/src/main/scala/code/api/openidconnect.scala index 2ed85dde2..6df50fdc2 100644 --- a/obp-api/src/main/scala/code/api/openidconnect.scala +++ b/obp-api/src/main/scala/code/api/openidconnect.scala @@ -186,7 +186,8 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { email = getClaim(name = "email", idToken = idToken), userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) } } diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 9cec32129..49ef8d0fb 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -198,7 +198,8 @@ object Consent { email = email, userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) } } diff --git a/obp-api/src/main/scala/code/model/BankingData.scala b/obp-api/src/main/scala/code/model/BankingData.scala index 07374a734..f6d0a66b2 100644 --- a/obp-api/src/main/scala/code/model/BankingData.scala +++ b/obp-api/src/main/scala/code/model/BankingData.scala @@ -199,6 +199,7 @@ case class BankAccountExtended(val bankAccount: BankAccount) extends MdcLoggable val createdByConsentId = None val createdByUserInvitationId = None val isDeleted = None + val lastMarketingAgreementSignedDate = None }) } else { accountHolders diff --git a/obp-api/src/main/scala/code/model/User.scala b/obp-api/src/main/scala/code/model/User.scala index 316cc2106..aff34e40a 100644 --- a/obp-api/src/main/scala/code/model/User.scala +++ b/obp-api/src/main/scala/code/model/User.scala @@ -131,7 +131,7 @@ object UserX { } def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], company: Option[String]) = { - Users.users.vend.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, None, company) + Users.users.vend.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, None, company, None) } def createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) = { diff --git a/obp-api/src/main/scala/code/model/dataAccess/ResourceUser.scala b/obp-api/src/main/scala/code/model/dataAccess/ResourceUser.scala index 6c8d72159..30d5d8a20 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/ResourceUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/ResourceUser.scala @@ -26,6 +26,8 @@ TESOBE (http://www.tesobe.com/) */ package code.model.dataAccess +import java.util.Date + import code.api.util.APIUtil import code.util.MappedUUID import com.openbankproject.commons.model.{User, UserPrimaryKey} @@ -81,6 +83,7 @@ class ResourceUser extends LongKeyedMapper[ResourceUser] with User with ManyToMa object IsDeleted extends MappedBoolean(this) { override def defaultValue = false } + object LastMarketingAgreementSignedDate extends MappedDate(this) def emailAddress = { val e = email.get @@ -109,6 +112,7 @@ class ResourceUser extends LongKeyedMapper[ResourceUser] with User with ManyToMa override def createdByConsentId = if(CreatedByConsentId.get == null) None else if (CreatedByConsentId.get.isEmpty) None else Some(CreatedByConsentId.get) //null --> None override def createdByUserInvitationId = if(CreatedByUserInvitationId.get == null) None else if (CreatedByUserInvitationId.get.isEmpty) None else Some(CreatedByUserInvitationId.get) //null --> None override def isDeleted: Option[Boolean] = if(IsDeleted.jdbcFriendly(IsDeleted.calcFieldName) == null) None else Some(IsDeleted.get) // null --> None + override def lastMarketingAgreementSignedDate: Option[Date] = if(IsDeleted.jdbcFriendly(LastMarketingAgreementSignedDate.calcFieldName) == null) None else Some(LastMarketingAgreementSignedDate.get) // null --> None } object ResourceUser extends ResourceUser with LongKeyedMetaMapper[ResourceUser]{ diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala index 50c9eef34..3b28059fb 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUsers.scala @@ -1,5 +1,7 @@ package code.remotedata +import java.util.Date + import akka.pattern.ask import code.actorsystem.ObpActorInit import code.api.util.OBPQueryParam @@ -79,8 +81,8 @@ object RemotedataUsers extends ObpActorInit with Users { res.mapTo[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] } - def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) : Box[ResourceUser] = getValueFromFuture( - (actor ? cc.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, createdByUserInvitationId, company)).mapTo[Box[ResourceUser]] + def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String], lastMarketingAgreementSignedDate: Option[Date]) : Box[ResourceUser] = getValueFromFuture( + (actor ? cc.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, createdByUserInvitationId, company, lastMarketingAgreementSignedDate)).mapTo[Box[ResourceUser]] ) def createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) : Box[ResourceUser] = getValueFromFuture( diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala b/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala index 696999375..5ed768852 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataUsersActor.scala @@ -1,5 +1,7 @@ package code.remotedata +import java.util.Date + import akka.actor.Actor import akka.pattern.pipe import code.actorsystem.ObpActorHelper @@ -87,9 +89,9 @@ class RemotedataUsersActor extends Actor with ObpActorHelper with MdcLoggable { logger.debug(s"getUsers(queryParams: ($queryParams))") mapper.getUsers(queryParams) pipeTo sender - case cc.createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) => - logger.debug("createResourceUser(" + provider + ", " + providerId.getOrElse("None") + ", " + name.getOrElse("None") + ", " + email.getOrElse("None") + ", " + userId.getOrElse("None") + ", " + createdByUserInvitationId.getOrElse("None") + ", " + company.getOrElse("None") + ")") - sender ! (mapper.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, createdByUserInvitationId, company)) + case cc.createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String], lastMarketingAgreementSignedDate: Option[Date]) => + logger.debug("createResourceUser(" + provider + ", " + providerId.getOrElse("None") + ", " + name.getOrElse("None") + ", " + email.getOrElse("None") + ", " + userId.getOrElse("None") + ", " + createdByUserInvitationId.getOrElse("None") + ", " + company.getOrElse("None") + ", " + lastMarketingAgreementSignedDate.getOrElse("None") + ")") + sender ! (mapper.createResourceUser(provider, providerId, createdByConsentId, name, email, userId, createdByUserInvitationId, company, lastMarketingAgreementSignedDate)) case cc.createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) => logger.debug("createUnsavedResourceUser(" + provider + ", " + providerId.getOrElse("None") + ", " + name.getOrElse("None") + ", " + email.getOrElse("None") + ", " + userId.getOrElse("None") + ")") diff --git a/obp-api/src/main/scala/code/snippet/UserInvitation.scala b/obp-api/src/main/scala/code/snippet/UserInvitation.scala index 9785fb22a..5cd61f183 100644 --- a/obp-api/src/main/scala/code/snippet/UserInvitation.scala +++ b/obp-api/src/main/scala/code/snippet/UserInvitation.scala @@ -27,6 +27,7 @@ TESOBE (http://www.tesobe.com/) package code.snippet import java.time.{Duration, ZoneId, ZoneOffset, ZonedDateTime} +import java.util.Date import code.api.util.{APIUtil, SecureRandomUtil} import code.model.dataAccess.{AuthUser, ResourceUser} @@ -99,7 +100,8 @@ class UserInvitation extends MdcLoggable { name = Some(usernameVar.is), email = Some(email), userInvitationId = userInvitation.map(_.userInvitationId).toOption, - company = userInvitation.map(_.company).toOption + company = userInvitation.map(_.company).toOption, + lastMarketingAgreementSignedDate = if(marketingInfoCheckboxVar.is) Some(new Date()) else None ).map{ u => // AuthUser table createAuthUser(user = u, firstName = firstNameVar.is, lastName = lastNameVar.is) match { @@ -211,7 +213,8 @@ class UserInvitation extends MdcLoggable { name: Option[String], email: Option[String], userInvitationId: Option[String], - company: Option[String] + company: Option[String], + lastMarketingAgreementSignedDate: Option[Date], ): Box[ResourceUser] = { Users.users.vend.createResourceUser( provider = provider, @@ -221,7 +224,8 @@ class UserInvitation extends MdcLoggable { email = email, userId = None, createdByUserInvitationId = userInvitationId, - company = company + company = company, + lastMarketingAgreementSignedDate = lastMarketingAgreementSignedDate ) } diff --git a/obp-api/src/main/scala/code/users/LiftUsers.scala b/obp-api/src/main/scala/code/users/LiftUsers.scala index e8dcf9f45..73e759889 100644 --- a/obp-api/src/main/scala/code/users/LiftUsers.scala +++ b/obp-api/src/main/scala/code/users/LiftUsers.scala @@ -1,5 +1,7 @@ package code.users +import java.util.Date + import code.api.util._ import code.entitlement.Entitlement import code.loginattempts.LoginAttempt.maxBadLoginAttempts @@ -60,7 +62,8 @@ object LiftUsers extends Users with MdcLoggable{ email = email, userId = None, createdByUserInvitationId = None, - company = None + company = None, + lastMarketingAgreementSignedDate = None ) (newUser, true) } @@ -210,7 +213,8 @@ object LiftUsers extends Users with MdcLoggable{ email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], - company: Option[String]): Box[ResourceUser] = { + company: Option[String], + lastMarketingAgreementSignedDate: Option[Date]): Box[ResourceUser] = { val ru = ResourceUser.create ru.provider_(provider) providerId match { @@ -241,6 +245,10 @@ object LiftUsers extends Users with MdcLoggable{ case Some(v) => ru.Company(v) case None => } + lastMarketingAgreementSignedDate match { + case Some(v) => ru.LastMarketingAgreementSignedDate(v) + case None => + } Full(ru.saveMe()) } diff --git a/obp-api/src/main/scala/code/users/Users.scala b/obp-api/src/main/scala/code/users/Users.scala index ef6344eb5..d6b446199 100644 --- a/obp-api/src/main/scala/code/users/Users.scala +++ b/obp-api/src/main/scala/code/users/Users.scala @@ -1,5 +1,7 @@ package code.users +import java.util.Date + import code.api.util.{APIUtil, OBPQueryParam} import code.entitlement.Entitlement import code.model.dataAccess.ResourceUser @@ -54,7 +56,15 @@ trait Users { def getUsers(queryParams: List[OBPQueryParam]): Future[List[(ResourceUser, Box[List[Entitlement]], Option[List[UserAgreement]])]] - def createResourceUser(provider: String, providerId: Option[String], createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) : Box[ResourceUser] + def createResourceUser(provider: String, + providerId: Option[String], + createdByConsentId: Option[String], + name: Option[String], + email: Option[String], + userId: Option[String], + createdByUserInvitationId: Option[String], + company: Option[String], + lastMarketingAgreementSignedDate: Option[Date]) : Box[ResourceUser] def createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) : Box[ResourceUser] @@ -85,7 +95,7 @@ class RemotedataUsersCaseClasses { case class getAllUsers() case class getAllUsersF(queryParams: List[OBPQueryParam]) case class getUsers(queryParams: List[OBPQueryParam]) - case class createResourceUser(provider: String, providerId: Option[String],createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String]) + case class createResourceUser(provider: String, providerId: Option[String],createdByConsentId: Option[String], name: Option[String], email: Option[String], userId: Option[String], createdByUserInvitationId: Option[String], company: Option[String], lastMarketingAgreementSignedDate: Option[Date]) case class createUnsavedResourceUser(provider: String, providerId: Option[String], name: Option[String], email: Option[String], userId: Option[String]) case class saveResourceUser(resourceUser: ResourceUser) case class deleteResourceUser(userId: Long) diff --git a/obp-api/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala b/obp-api/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala index 79aac35ef..580a02a46 100644 --- a/obp-api/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala +++ b/obp-api/src/test/scala/code/bankaccountcreation/BankAccountCreationListenerTest.scala @@ -38,7 +38,7 @@ class BankAccountCreationListenerTest extends ServerSetup with DefaultConnectorT //need to create the user for the bank accout creation process to work def getTestUser() = Users.users.vend.getUserByProviderId(userProvider, userId).getOrElse { - Users.users.vend.createResourceUser(userProvider, Some(userId), None, None, None, None, None, None).openOrThrowException(attemptedToOpenAnEmptyBox) + Users.users.vend.createResourceUser(userProvider, Some(userId), None, None, None, None, None, None, None).openOrThrowException(attemptedToOpenAnEmptyBox) } val expectedBankId = "quxbank" diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala index 8dceecd5e..048be111e 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala @@ -802,7 +802,16 @@ case class Transaction( val accountId = thisAccount.accountId } -case class UserCommons(userPrimaryKey : UserPrimaryKey, userId: String,idGivenByProvider: String, provider : String, emailAddress : String, name : String, createdByConsentId: Option[String] = None, createdByUserInvitationId: Option[String] = None, isDeleted: Option[Boolean] = None) extends User +case class UserCommons(userPrimaryKey : UserPrimaryKey, + userId: String, + idGivenByProvider: String, + provider : String, + emailAddress : String, + name : String, + createdByConsentId: Option[String] = None, + createdByUserInvitationId: Option[String] = None, + isDeleted: Option[Boolean] = None, + lastMarketingAgreementSignedDate: Option[Date] = None) extends User case class InternalBasicUser( userId:String, diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/UserModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/UserModel.scala index bf6a26021..4452e3b3d 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/UserModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/UserModel.scala @@ -26,6 +26,8 @@ TESOBE (http://www.tesobe.com/) package com.openbankproject.commons.model +import java.util.Date + /** * An O-R mapped "User" class that includes first name, last name, password * @@ -66,6 +68,7 @@ trait User { def isOriginalUser = createdByConsentId.isEmpty def isConsentUser = createdByConsentId.nonEmpty def isDeleted: Option[Boolean] + def lastMarketingAgreementSignedDate: Option[Date] } case class UserPrimaryKey(val value : Long) { From ad4bfc125ca5432a5c3104c0245ae2770b6a2e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 16 Sep 2021 17:40:08 +0200 Subject: [PATCH 020/185] feature/Add last_marketing_agreement_signed_date to the users JSON --- .../code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala | 6 ++++-- .../src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 60e866dad..583196c6f 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -1711,7 +1711,8 @@ object SwaggerDefinitionsJSON { entitlements = entitlementJSONs, views = None, agreements = None, - is_deleted = false + is_deleted = false, + last_marketing_agreement_signed_date = Some(DateWithDayExampleObject) ) val userJsonWithAgreementsV400 = UserJsonV400( user_id = ExampleValue.userIdExample.value, @@ -1722,7 +1723,8 @@ object SwaggerDefinitionsJSON { entitlements = entitlementJSONs, views = None, agreements = Some(Nil), - is_deleted = false + is_deleted = false, + last_marketing_agreement_signed_date = Some(DateWithDayExampleObject) ) val userIdJsonV400 = UserIdJsonV400( user_id = ExampleValue.userIdExample.value diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index 05713e556..f855ba317 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -899,7 +899,8 @@ case class UserJsonV400( entitlements : EntitlementJSONs, views: Option[ViewsJSON300], agreements: Option[List[UserAgreementJson]], - is_deleted: Boolean + is_deleted: Boolean, + last_marketing_agreement_signed_date: Option[Date] ) case class UsersJsonV400(users: List[UserJsonV400]) @@ -917,7 +918,8 @@ object JSONFactory400 { agreements = agreements.map(_.map( i => UserAgreementJson(`type` = i.agreementType, text = i.agreementText)) ), - is_deleted = user.isDeleted.getOrElse(false) + is_deleted = user.isDeleted.getOrElse(false), + last_marketing_agreement_signed_date = user.lastMarketingAgreementSignedDate ) } From e65cd51d37fd0329b318f2456a81d8e265375173 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 17 Sep 2021 14:26:35 +0200 Subject: [PATCH 021/185] feature/added the new props webui_main_faq_external_link --- .../resources/props/sample.props.template | 5 + .../src/main/scala/code/snippet/WebUI.scala | 16 ++ obp-api/src/main/webapp/index.html | 161 +----------------- obp-api/src/main/webapp/mainFaq.html | 160 +++++++++++++++++ 4 files changed, 182 insertions(+), 160 deletions(-) create mode 100644 obp-api/src/main/webapp/mainFaq.html diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index c5fefe45c..039fc9e93 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -420,6 +420,11 @@ webui_sdks_url = https://github.com/OpenBankProject/OBP-API/wiki/OAuth-Client-SD # then OBP-API can show the content to the HomePage `SDK Showcases`. Please check it over the sandbox homepage first. #webui_featured_sdks_external_link = https://static.openbankproject.com/obp/sdks.html + +# the external html page for the FAQ section. the default link is the obp one. Please following the div to modify it. This link should be anonymous access. +# then OBP-API can show the content to the HomePage `FAQs`. Please check it over the sandbox homepage first. +#webui_main_faq_external_link = /mainFaq.html + # Text about data in FAQ webui_faq_data_text = We use real data and customer profiles which have been anonymized. diff --git a/obp-api/src/main/scala/code/snippet/WebUI.scala b/obp-api/src/main/scala/code/snippet/WebUI.scala index abcc9b833..165c526df 100644 --- a/obp-api/src/main/scala/code/snippet/WebUI.scala +++ b/obp-api/src/main/scala/code/snippet/WebUI.scala @@ -142,7 +142,23 @@ class WebUI extends MdcLoggable{ "#main-showcases *" #> scala.xml.Unparsed(sdksHtmlContent) } + val mainFaqHtmlLink = getWebUiPropsValue("webui_main_faq_external_link","") + + val mainFaqHtmlContent = try{ + if (mainFaqHtmlLink.isEmpty)//If the webui_featured_sdks_external_link is not set, we will read the internal sdks.html file instead. + LiftRules.getResource("/mainFaq.html").map{ url => + Source.fromURL(url, "UTF-8").mkString + }.openOrThrowException("Please check the content of this file: src/main/webapp/mainFaq.html") + else + Source.fromURL(sdksHtmlLink, "UTF-8").mkString + }catch { + case _ : Throwable => "

SDK Showcases is wrong, please check the props `webui_featured_sdks_external_link`

" + } + // webui_featured_sdks_external_link props, we can set the sdks here. check the `SDK Showcases` in Homepage, and you can see all the sdks. + def mainFaqHtml: CssSel = { + "#main-faq *" #> scala.xml.Unparsed(mainFaqHtmlContent) + } val brandString = activeBrand match { case Some(v) => s"&brand=$v" diff --git a/obp-api/src/main/webapp/index.html b/obp-api/src/main/webapp/index.html index f3c9fea12..dec65bd91 100644 --- a/obp-api/src/main/webapp/index.html +++ b/obp-api/src/main/webapp/index.html @@ -255,166 +255,7 @@ Berlin 13359, Germany -
-

FAQs

- -
-
- -
-
- -
-
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
+
diff --git a/obp-api/src/main/webapp/mainFaq.html b/obp-api/src/main/webapp/mainFaq.html new file mode 100644 index 000000000..97737a6e1 --- /dev/null +++ b/obp-api/src/main/webapp/mainFaq.html @@ -0,0 +1,160 @@ +

FAQs-xxxxx

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
From b0ae1ca5c128647044968734ed82b50c802b9ed2 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 17 Sep 2021 14:33:37 +0200 Subject: [PATCH 022/185] docfix/added the webui_main_faq_external_link to release_notes.md --- release_notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release_notes.md b/release_notes.md index acf52bb2c..3919b7f94 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,6 +3,7 @@ ### Most recent changes at top of file ``` Date Commit Action +17/09/2021 e65cd51d Added props: webui_main_faq_external_link, default is obp static file: /mainFaq.html 09/09/2021 65952225 Added props: webui_support_email, default is contact@openbankproject.com 02/09/2021 a826d908 Renamed Web UI props: webui_post_user_invitation_privacy_conditions_value => webui_privacy_policy From 7ebb47b55093e3010bc827719411a3f777c8ff74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 20 Sep 2021 09:03:53 +0200 Subject: [PATCH 023/185] bugfix/Use subject instead of given_name for username --- obp-api/src/main/scala/code/api/openidconnect.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/openidconnect.scala b/obp-api/src/main/scala/code/api/openidconnect.scala index 6df50fdc2..a6ef5051a 100644 --- a/obp-api/src/main/scala/code/api/openidconnect.scala +++ b/obp-api/src/main/scala/code/api/openidconnect.scala @@ -182,7 +182,7 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { provider = issuer, providerId = subject, createdByConsentId = None, - name = getClaim(name = "given_name", idToken = idToken).orElse(subject), + name = subject, email = getClaim(name = "email", idToken = idToken), userId = None, createdByUserInvitationId = None, From 282b1af7ce452d7ee03f19f0f162e58dc4db536d Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 20 Sep 2021 11:51:21 +0200 Subject: [PATCH 024/185] factor/tweaked mainFaq.html -> main-faq.html --- obp-api/src/main/resources/props/sample.props.template | 2 +- obp-api/src/main/scala/code/snippet/WebUI.scala | 4 ++-- obp-api/src/main/webapp/{mainFaq.html => main-faq.html} | 2 +- release_notes.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename obp-api/src/main/webapp/{mainFaq.html => main-faq.html} (99%) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 039fc9e93..2cd0951af 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -423,7 +423,7 @@ webui_sdks_url = https://github.com/OpenBankProject/OBP-API/wiki/OAuth-Client-SD # the external html page for the FAQ section. the default link is the obp one. Please following the div to modify it. This link should be anonymous access. # then OBP-API can show the content to the HomePage `FAQs`. Please check it over the sandbox homepage first. -#webui_main_faq_external_link = /mainFaq.html +#webui_main_faq_external_link = /main-faq.html # Text about data in FAQ webui_faq_data_text = We use real data and customer profiles which have been anonymized. diff --git a/obp-api/src/main/scala/code/snippet/WebUI.scala b/obp-api/src/main/scala/code/snippet/WebUI.scala index 165c526df..df1722300 100644 --- a/obp-api/src/main/scala/code/snippet/WebUI.scala +++ b/obp-api/src/main/scala/code/snippet/WebUI.scala @@ -146,9 +146,9 @@ class WebUI extends MdcLoggable{ val mainFaqHtmlContent = try{ if (mainFaqHtmlLink.isEmpty)//If the webui_featured_sdks_external_link is not set, we will read the internal sdks.html file instead. - LiftRules.getResource("/mainFaq.html").map{ url => + LiftRules.getResource("/main-faq.html").map{ url => Source.fromURL(url, "UTF-8").mkString - }.openOrThrowException("Please check the content of this file: src/main/webapp/mainFaq.html") + }.openOrThrowException("Please check the content of this file: src/main/webapp/main-faq.html") else Source.fromURL(sdksHtmlLink, "UTF-8").mkString }catch { diff --git a/obp-api/src/main/webapp/mainFaq.html b/obp-api/src/main/webapp/main-faq.html similarity index 99% rename from obp-api/src/main/webapp/mainFaq.html rename to obp-api/src/main/webapp/main-faq.html index 97737a6e1..769160174 100644 --- a/obp-api/src/main/webapp/mainFaq.html +++ b/obp-api/src/main/webapp/main-faq.html @@ -1,4 +1,4 @@ -

FAQs-xxxxx

+

FAQs

diff --git a/release_notes.md b/release_notes.md index 3919b7f94..4159cc333 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,7 +3,7 @@ ### Most recent changes at top of file ``` Date Commit Action -17/09/2021 e65cd51d Added props: webui_main_faq_external_link, default is obp static file: /mainFaq.html +17/09/2021 e65cd51d Added props: webui_main_faq_external_link, default is obp static file: /main-faq.html 09/09/2021 65952225 Added props: webui_support_email, default is contact@openbankproject.com 02/09/2021 a826d908 Renamed Web UI props: webui_post_user_invitation_privacy_conditions_value => webui_privacy_policy From d2ab9652edf99289ba321d7dbcfaafc3f8ce6f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 20 Sep 2021 11:51:44 +0200 Subject: [PATCH 025/185] feature/Allow an email as a valid username --- obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index 2bed203ff..121b96988 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -140,6 +140,7 @@ class AuthUser extends MegaProtoUser[AuthUser] with MdcLoggable { } /** + * Username is a valid email address or the regex below: * Regex to validate a username * * ^(?=.{8,100}$)(?![_.])(?!.*[_.]{2})[a-zA-Z0-9._]+(? String)(e: String) = e match { case null => List(FieldError(this, Text(msg))) case e if e.trim.isEmpty => List(FieldError(this, Text(msg))) + case e if emailRegex.findFirstMatchIn(e).isDefined => Nil // Email is valid username case e if usernameRegex.findFirstMatchIn(e).isDefined => Nil case _ => List(FieldError(this, Text(msg))) } From 85e205e9dfd2b46efcebae4b91c28d20826ec258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 21 Sep 2021 13:31:08 +0200 Subject: [PATCH 026/185] feature/Make OBP API return Bad Request on Duplicate Query Parameters or Header keys --- .../main/scala/code/api/util/APIUtil.scala | 42 +++++++++++++++++++ .../scala/code/api/util/ErrorMessages.scala | 2 + 2 files changed, 44 insertions(+) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index ee9975643..3f7dd48e8 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3848,6 +3848,40 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } } } + /** + * validate whether current request's query parameters + * @param operationId + * @param callContext + * @return Full(errorResponse) if validate fail + */ + def validateQueryParams(operationId: String, callContext: CallContext): Box[JsonResponse] = { + val queryString: String = if (callContext.url.contains("?")) callContext.url.split("\\?",2)(1) else "" + val queryParams: Array[String] = queryString.split("&").map(_.split("=")(0)) + val queryParamsGrouped: Map[String, Array[String]] = queryParams.groupBy(x => x) + queryParamsGrouped.toList.forall(_._2.size == 1) match { + case true => Empty + case false => + Box.tryo( + createErrorJsonResponse(s"${ErrorMessages.DuplicatedQueryParameters}", 400, callContext.correlationId) + ) + } + } + /** + * validate whether current request's header keys + * @param operationId + * @param callContext + * @return Full(errorResponse) if validate fail + */ + def validateRequestHeadersKeys(operationId: String, callContext: CallContext): Box[JsonResponse] = { + val headerKeysGrouped: Map[String, List[HTTPParam]] = callContext.requestHeaders.groupBy(x => x.name) + headerKeysGrouped.toList.forall(_._2.size == 1) match { + case true => Empty + case false => + Box.tryo( + createErrorJsonResponse(s"${ErrorMessages.DuplicatedHeaderKeys}", 400, callContext.correlationId) + ) + } + } def createErrorJsonResponse(errorMsg: String, errorCode: Int, correlationId: String): JsonResponse = { import net.liftweb.json.JsonDSL._ @@ -3911,6 +3945,14 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ // validate auth type { case (Some(callContext), operationId) => validateAuthType(operationId, callContext) + }, + // validate query params + { + case (Some(callContext), operationId) => validateQueryParams(operationId, callContext) + }, + // validate request header keys + { + case (Some(callContext), operationId) => validateRequestHeadersKeys(operationId, callContext) } ) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 474afa523..33cc20776 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -58,6 +58,8 @@ object ErrorMessages { val InvalidRequestPayload = "OBP-09014: Incorrect request body Format, it should be a valid json that matches Validation rule." val DynamicDataNotFound = "OBP-09015: Dynamic Data not found. Please specify a valid value." + val DuplicatedQueryParameters = "OBP-09016: Duplicated Query Parameters are not allowed." + val DuplicatedHeaderKeys = "OBP-09017: Duplicated Header Keys are not allowed." // General messages (OBP-10XXX) From 9bc3af947435927f4d6984e3d539e6c83a460f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 22 Sep 2021 07:24:16 +0200 Subject: [PATCH 027/185] refactor/Tweak error messages Duplicated => Duplicate --- obp-api/src/main/scala/code/api/util/APIUtil.scala | 4 ++-- obp-api/src/main/scala/code/api/util/ErrorMessages.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 3f7dd48e8..32ef1d00e 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3862,7 +3862,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case true => Empty case false => Box.tryo( - createErrorJsonResponse(s"${ErrorMessages.DuplicatedQueryParameters}", 400, callContext.correlationId) + createErrorJsonResponse(s"${ErrorMessages.DuplicateQueryParameters}", 400, callContext.correlationId) ) } } @@ -3878,7 +3878,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case true => Empty case false => Box.tryo( - createErrorJsonResponse(s"${ErrorMessages.DuplicatedHeaderKeys}", 400, callContext.correlationId) + createErrorJsonResponse(s"${ErrorMessages.DuplicateHeaderKeys}", 400, callContext.correlationId) ) } } diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 33cc20776..aada7e286 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -58,8 +58,8 @@ object ErrorMessages { val InvalidRequestPayload = "OBP-09014: Incorrect request body Format, it should be a valid json that matches Validation rule." val DynamicDataNotFound = "OBP-09015: Dynamic Data not found. Please specify a valid value." - val DuplicatedQueryParameters = "OBP-09016: Duplicated Query Parameters are not allowed." - val DuplicatedHeaderKeys = "OBP-09017: Duplicated Header Keys are not allowed." + val DuplicateQueryParameters = "OBP-09016: Duplicate Query Parameters are not allowed." + val DuplicateHeaderKeys = "OBP-09017: Duplicate Header Keys are not allowed." // General messages (OBP-10XXX) From 09e44eba249954085cd5eba9ac3a31c785725da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 22 Sep 2021 09:34:48 +0200 Subject: [PATCH 028/185] refactor/User Invitation Page --- .../scala/code/snippet/UserInvitation.scala | 6 +- .../src/main/webapp/media/css/data-area.css | 151 ++++++++++++++++++ obp-api/src/main/webapp/media/css/website.css | 2 + obp-api/src/main/webapp/media/js/website.js | 7 + obp-api/src/main/webapp/user-invitation.html | 12 +- 5 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 obp-api/src/main/webapp/media/css/data-area.css diff --git a/obp-api/src/main/scala/code/snippet/UserInvitation.scala b/obp-api/src/main/scala/code/snippet/UserInvitation.scala index 5cd61f183..7ff0609e7 100644 --- a/obp-api/src/main/scala/code/snippet/UserInvitation.scala +++ b/obp-api/src/main/scala/code/snippet/UserInvitation.scala @@ -128,9 +128,9 @@ class UserInvitation extends MdcLoggable { } def showError(usernameError: String) = { - S.error("register-consumer-errors", usernameError) + S.error("data-area-errors", usernameError) register & - "#register-consumer-errors *" #> { + "#data-area-errors *" #> { ".error *" #> List(usernameError).map({ e => ".errorContent *" #> e @@ -170,7 +170,7 @@ class UserInvitation extends MdcLoggable { "#marketing_info_checkbox" #> SHtml.checkbox(marketingInfoCheckboxVar, marketingInfoCheckboxVar(_)) & "type=submit" #> SHtml.submit(s"$registrationConsumerButtonValue", () => submitButtonDefense) } & - "#register-consumer-success" #> "" + "#data-area-success" #> "" } userInvitation match { case Full(payload) if payload.status == "CREATED" => // All good diff --git a/obp-api/src/main/webapp/media/css/data-area.css b/obp-api/src/main/webapp/media/css/data-area.css new file mode 100644 index 000000000..a3cd199ca --- /dev/null +++ b/obp-api/src/main/webapp/media/css/data-area.css @@ -0,0 +1,151 @@ +#data-area { + background-color: #53C4EF; + padding: 20px 10px; + color: white; + margin: 30px 0 90px; +} + +#data-area #data-area-explanation, +#data-area form, +#data-area-errors, +#data-area-success { + max-width: 610px; + margin: 0 auto; + color: #333333; +} +#data-area-errors span{ + color: black; +} +#data-area h1 { + font-family: Roboto-Light; + font-size: 28px; + color: #333333; + letter-spacing: 0; + line-height: 36px; + text-align: left; + margin-bottom: 32px; + font-weight: normal; +} + +#data-area #data-area-explanation p:nth-child(2) { + font-family: Roboto-Light; + font-size: 22px; + color: #333333; + letter-spacing: 0; + line-height: 31px; + margin-bottom: 8px; + font-weight: normal; + text-align: left; +} + +#data-area #data-area-explanation p:last-child{ + margin-top: 8px; + font-family: Roboto-Regular; + font-size: 14px; + color: #333333; + letter-spacing: 0; + line-height: 20px; + margin-bottom: 32px; +} + +#data-area-input form label{ + margin-bottom: 8px; + margin-top: 17px; + font-family: Roboto-Regular; + font-size: 16px; + color: #333333; + line-height: 24px; +} + +#data-area-input form select{ + height: 44px; + border: 1px solid #767676; + -webkit-appearance: none; + -webkit-border-radius: 0px; +} + +#data-area #data-area-explanation { + margin-top: 32px; + margin-bottom: 20px; + color: #333333; + padding: 0 15px; +} + +#data-area textarea { + height: 96px; + border: 1px solid #767676; + border-radius: 0px; +} +#data-area #data-area-errors { + margin-bottom: 20px; +} +#data-area #data-area-success { + margin-top: 30px; +} + +#data-area #data-area-success h1 { + padding-left: 15px; +} + +#data-area #data-area-success #data-area-success-message{ + font-family: Roboto-Light; + font-size: 22px; + color: #333333; + letter-spacing: 0; + line-height: 31px; + padding-left: 15px; +} + + +#data-area #data-area-success p { + font-family: Roboto-Light; + font-size: 22px; + color: #333333; + letter-spacing: 0; + margin-bottom: 44px; + line-height: 31px; + padding-left: 15px; +} + +#data-area-success a{ + text-decoration:underline; +} + + +#data-area-success span, +#data-area-success a{ + font-family: Roboto-Regular; + font-size: 16px; + color: #333333; + line-height: 24px; +} +#data-area #data-area-success .row { + margin-bottom: 20px; +} +#data-area #data-area-success .row div:nth-child(1) { + font-family: Roboto-Medium; + font-size: 16px; + color: #333333; + line-height: 24px; +} +#data-area-input form .btn-danger{ + margin-left: 0; + margin-top: 33px; +} + +#data-area-input #data-area-errors-div{ + margin-top: 8px; +} + +#data-area-input #data-area-errors{ + font-family: Roboto-Regular; + font-size: 14px; + color: #333333; + line-height: 20px; + background-image: url(/media/images/icons/status_error_onlight.svg); + background-repeat:no-repeat; + background-position: left 12px; + background-size: 18px 15.70px; + padding-top: 8px; + padding-left: 26px; +} \ No newline at end of file diff --git a/obp-api/src/main/webapp/media/css/website.css b/obp-api/src/main/webapp/media/css/website.css index 0bd4ea9b1..c4a621bd8 100644 --- a/obp-api/src/main/webapp/media/css/website.css +++ b/obp-api/src/main/webapp/media/css/website.css @@ -8,6 +8,7 @@ @import url(/media/css/main-start.css); @import url(/media/css/authorise.css); @import url(/media/css/register-consumer.css); +@import url(/media/css/data-area.css); @import url(/media/css/signup.css); @import url(/media/css/recover-password.css); @import url(/media/css/main-showcases.css); @@ -284,6 +285,7 @@ header #header-decoration, #signup, #recover-password, #register-consumer, +#data-area , #create-account, .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus, diff --git a/obp-api/src/main/webapp/media/js/website.js b/obp-api/src/main/webapp/media/js/website.js index a740571c9..ac02198c5 100644 --- a/obp-api/src/main/webapp/media/js/website.js +++ b/obp-api/src/main/webapp/media/js/website.js @@ -340,6 +340,13 @@ $(document).ready(function() { } else{ consumerRegistrationAppRequestUriError.parent().addClass('hide'); } + + var dataAreaErrors = $('#data-area-input #data-area-errors'); + if (dataAreaErrors.length > 0 && dataAreaErrors.html().length > 0) { + dataAreaErrors.parent().removeClass('hide'); + } else{ + dataAreaErrors.parent().addClass('hide'); + } { var consumerRegistrationJwksError = $('#register-consumer-input #consumer-registration-app-signing_jwks-error'); diff --git a/obp-api/src/main/webapp/user-invitation.html b/obp-api/src/main/webapp/user-invitation.html index 29bd87f1d..1d8901434 100644 --- a/obp-api/src/main/webapp/user-invitation.html +++ b/obp-api/src/main/webapp/user-invitation.html @@ -23,11 +23,11 @@ Berlin 13359, Germany This product includes software developed at TESOBE (http://www.tesobe.com/) --> -
+
-
-
+
+

Complete your user invitation

Please complete the information about the user invitation application below.

All fields are required unless marked as 'optional'

@@ -94,9 +94,9 @@ Berlin 13359, Germany
- -
- + +
+
From 071b28a633bc9629e200cbe86b6f62ad9cd9243e Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 15 Oct 2021 12:22:38 +0200 Subject: [PATCH 073/185] docfix/typo --- obp-api/src/main/scala/code/api/util/Glossary.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 00a82de98..2bd3a303e 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2287,7 +2287,7 @@ object Glossary extends MdcLoggable { | |To enable Endpoint Mapping for your Dynamic Endpoints, either set the `host` in the swagger file to "dynamic_entity" upon creation of the Dynamic Endpoints - or update the host using the Update Dynamic Endpoint Host endpoints. | - |Once the `host` is thus set, you can the Endpoint Mapping endpoints to map the Dynamic Endpoint fields to Dynamic Entity data. + |Once the `host` is thus set, you can use the Endpoint Mapping endpoints to map the Dynamic Endpoint fields to Dynamic Entity data. | |See the [Create Endpoint Mapping](/index#OBPv4.0.0-createEndpointMapping) JSON body. You will need to know the operation_id in advance and you can prepare the request_mapping and response_mapping objects. You can get the operation ID from the API Explorer or Get Dynamic Endpoints endpoints. | From 3a95a6d6267e7c2e26b189554f92f3c73e099db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 18 Oct 2021 12:07:51 +0200 Subject: [PATCH 074/185] feature/Add link to the Hola app i.e. webui_api_hola_url props --- obp-api/src/main/resources/props/sample.props.template | 3 +++ obp-api/src/main/scala/code/snippet/WebUI.scala | 4 ++++ obp-api/src/main/webapp/index.html | 2 ++ 3 files changed, 9 insertions(+) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 816064d10..c88d5ffa0 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -387,6 +387,9 @@ webui_obp_cli_url = https://github.com/OpenBankProject/OBP-CLI # API Tester URL, change to your instance webui_api_tester_url = https://apitester.openbankproject.com +# API Hola app URL, change to your instance +webui_api_hola_url = # + diff --git a/obp-api/src/main/scala/code/snippet/WebUI.scala b/obp-api/src/main/scala/code/snippet/WebUI.scala index d225a9a3d..a3581422d 100644 --- a/obp-api/src/main/scala/code/snippet/WebUI.scala +++ b/obp-api/src/main/scala/code/snippet/WebUI.scala @@ -193,6 +193,10 @@ class WebUI extends MdcLoggable{ def apiTesterLink: CssSel = { ".api-tester-link a [href]" #> scala.xml.Unparsed(getWebUiPropsValue("webui_api_tester_url", "")) } + // Link to Hola app + def apiHolaLink: CssSel = { + ".api-hola-link a [href]" #> scala.xml.Unparsed(getWebUiPropsValue("webui_api_hola_url", "#")) + } // Link to API def apiLink: CssSel = { diff --git a/obp-api/src/main/webapp/index.html b/obp-api/src/main/webapp/index.html index dec65bd91..4a5944c90 100644 --- a/obp-api/src/main/webapp/index.html +++ b/obp-api/src/main/webapp/index.html @@ -325,6 +325,8 @@ Berlin 13359, Germany href="">OBP CLI API Tester + Hola
From c32e8c26084b304fc871ac9444ebe406c1e3ff93 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 18 Oct 2021 13:04:23 +0200 Subject: [PATCH 075/185] feature/added the sort_direction parameter and ORDER BY accountId for fastFirehoseAccounts --- .../main/scala/code/api/v4_0_0/APIMethods400.scala | 11 ++--------- .../code/bankconnectors/LocalMappedConnector.scala | 10 ++++++++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index f28c9fe16..ef83826cc 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -3327,14 +3327,7 @@ trait APIMethods400 { |This endpoint allows bulk access to accounts. | |optional pagination parameters for filter with accounts - |${urlParametersDocument(true, false) - .replace(s""" - | - |* sort_direction=ASC/DESC ==> default value: DESC. - | - |eg2:?limit=100&offset=0&sort_direction=ASC - | - |""". stripMargin,"")} + |${urlParametersDocument(true, false)} | |${authenticationRequiredMessage(true)} | @@ -3355,7 +3348,7 @@ trait APIMethods400 { _ <- Helper.booleanToFuture(failMsg = AccountFirehoseNotAllowedOnThisInstance, cc=cc.callContext) { allowAccountFirehose } - allowedParams = List("limit", "offset") + allowedParams = List("limit", "offset", "sort_direction") httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url) obpQueryParams <- NewStyle.function.createObpParams(httpParams, allowedParams, callContext) (firehoseAccounts, callContext) <- NewStyle.function.getBankAccountsWithAttributes(bankId, obpQueryParams, cc.callContext) diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index cf25e3a6a..32d945ecd 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -832,14 +832,20 @@ object LocalMappedConnector extends Connector with MdcLoggable { override def getBankAccountsWithAttributes(bankId: BankId, queryParams: List[OBPQueryParam], callContext: Option[CallContext]): OBPReturnType[Box[List[FastFirehoseAccount]]] = Future{ - val limit = queryParams.collect { case OBPLimit(value) => value }.headOption.getOrElse(1000) + val limit = queryParams.collect { case OBPLimit(value) => value }.headOption.getOrElse(50) val offset = queryParams.collect { case OBPOffset(value) => value }.headOption.getOrElse(0) + val orderBy = queryParams.collect { + case OBPOrdering(_, OBPDescending) => "DESC" + }.headOption.getOrElse("ASC") + + val ordering = if (orderBy =="DESC" ) sqls"DESC" else sqls"ASC" val firehoseAccounts = { scalikeDB readOnly { implicit session => - val sqlResult =sql""" + val sqlResult = sql""" select * from v_fast_firehose_accounts WHERE v_fast_firehose_accounts.bank_id = ${bankId.value} + ORDER BY v_fast_firehose_accounts.account_id $ordering LIMIT $limit OFFSET $offset """.stripMargin From 80ec3716ed2ad084b7e9c888d14e8d8e49aa043c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 19 Oct 2021 07:39:07 +0200 Subject: [PATCH 076/185] feature/3rd party developer consents to collecting personal data 2 --- obp-api/src/main/resources/props/sample.props.template | 4 ++-- obp-api/src/main/scala/code/snippet/UserInvitation.scala | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index c88d5ffa0..8ea3dc84f 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1083,5 +1083,5 @@ webui_developer_user_invitation_email_html_text=\ \ -# List of cotries where consent to collecting personal data is not mandatory -consent_to_collecting_personal_data_list = Austria, Belgium, Bulgaria, Croatia, Republic of Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden \ No newline at end of file +# List of countries where consent is not required for the collection of personal data +personal_data_collection_consent_country_waiver_list = Austria, Belgium, Bulgaria, Croatia, Republic of Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden, England, Scotland, Wales, Northern Ireland \ No newline at end of file diff --git a/obp-api/src/main/scala/code/snippet/UserInvitation.scala b/obp-api/src/main/scala/code/snippet/UserInvitation.scala index 1d8d018ad..5f393d155 100644 --- a/obp-api/src/main/scala/code/snippet/UserInvitation.scala +++ b/obp-api/src/main/scala/code/snippet/UserInvitation.scala @@ -64,7 +64,7 @@ class UserInvitation extends MdcLoggable { val privacyConditionsValue: String = getWebUiPropsValue("webui_privacy_policy", "") val termsAndConditionsValue: String = getWebUiPropsValue("webui_terms_and_conditions", "") val termsAndConditionsCheckboxValue: String = getWebUiPropsValue("webui_post_user_invitation_terms_and_conditions_checkbox_value", "I agree to the above Developer Terms and Conditions") - val consentExclusionList = getWebUiPropsValue("consent_to_collecting_personal_data_list", "").split(",").toList.map(_.trim) + val personalDataCollectionConsentCountryWaiverList = getWebUiPropsValue("personal_data_collection_consent_country_waiver_list", "").split(",").toList.map(_.trim) def registerForm: CssSel = { @@ -80,7 +80,7 @@ class UserInvitation extends MdcLoggable { countryVar.set(userInvitation.map(_.country).getOrElse("None")) // Propose the username only for the first time. In case an end user manually change it we must not override it. if(usernameVar.isEmpty) usernameVar.set(firstNameVar.is.toLowerCase + "." + lastNameVar.is.toLowerCase()) - if(consentExclusionList.exists(_.toLowerCase == countryVar.is.toLowerCase) == true) { + if(personalDataCollectionConsentCountryWaiverList.exists(_.toLowerCase == countryVar.is.toLowerCase) == true) { consentForCollectingMandatoryCheckboxVar.set(false) } else { consentForCollectingMandatoryCheckboxVar.set(true) @@ -100,7 +100,7 @@ class UserInvitation extends MdcLoggable { else if(Users.users.vend.getUserByUserName(usernameVar.is).isDefined) showErrorsForUsername() else if(privacyCheckboxVar.is == false) showErrorsForPrivacyConditions() else if(termsCheckboxVar.is == false) showErrorsForTermsAndConditions() - else if(consentExclusionList.exists(_.toLowerCase == countryVar.is.toLowerCase) == false && consentForCollectingCheckboxVar.is == false) showErrorsForConsentForCollectingPersonalData() + else if(personalDataCollectionConsentCountryWaiverList.exists(_.toLowerCase == countryVar.is.toLowerCase) == false && consentForCollectingCheckboxVar.is == false) showErrorsForConsentForCollectingPersonalData() else { // Resource User table createResourceUser( From 591a4e768cb966cdb5bcce0f72ad0b14e6c63d90 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 19 Oct 2021 13:19:19 +0200 Subject: [PATCH 077/185] feature/added the addFastFirehoseAccountsMaterializedView --- .../code/api/util/migration/Migration.scala | 13 +++ ...rationOfFastFireHoseMaterializedView.scala | 104 ++++++++++++++++++ .../bankconnectors/LocalMappedConnector.scala | 6 +- 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index 0e8d582b2..f2d37d31b 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -86,6 +86,7 @@ object Migration extends MdcLoggable { alterColumnEmailAtResourceUser(startedBeforeSchemifier) alterColumnNameAtProductFee(startedBeforeSchemifier) addFastFirehoseAccountsView(startedBeforeSchemifier) + addFastFirehoseAccountsMaterializedView(startedBeforeSchemifier) } private def dummyScript(): Boolean = { @@ -308,6 +309,18 @@ object Migration extends MdcLoggable { } } + private def addFastFirehoseAccountsMaterializedView(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.addfastFirehoseAccountsMaterializedView(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(addFastFirehoseAccountsMaterializedView(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfFastFireHoseMaterializedView.addFastFireHoseMaterializedView(name) + } + } + } + } /** diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala new file mode 100644 index 000000000..9ee9fb6cc --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala @@ -0,0 +1,104 @@ +package code.api.util.migration + +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.productfee.ProductFee +import net.liftweb.mapper.{DB, Schemifier} +import net.liftweb.util.DefaultConnectionIdentifier + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} + +object MigrationOfFastFireHoseMaterializedView { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def addFastFireHoseMaterializedView(name: String): Boolean = { + DbFunction.tableExists(ProductFee, (DB.use(DefaultConnectionIdentifier){ conn => conn})) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { + () => + """ + |CREATE MATERIALIZED VIEW mv_fast_firehose_accounts AS select + | mappedbankaccount.theaccountid as account_id, + | mappedbankaccount.bank as bank_id, + | mappedbankaccount.accountlabel as account_label, + | mappedbankaccount.accountnumber as account_number, + | (select + | string_agg( + | 'user_id:' + | || resourceuser.userid_ + | ||',provider:' + | ||resourceuser.provider_ + | ||',user_name:' + | ||resourceuser.name_, + | ',') as owners + | from resourceuser + | where + | resourceuser.id = mapperaccountholders.user_c + | ), + | mappedbankaccount.kind as kind, + | mappedbankaccount.accountcurrency as account_currency , + | mappedbankaccount.accountbalance as account_balance, + | (select + | string_agg( + | 'bank_id:' + | ||bankaccountrouting.bankid + | ||',account_id:' + | ||bankaccountrouting.accountid, + | ',' + | ) as account_routings + | from bankaccountrouting + | where + | bankaccountrouting.accountid = mappedbankaccount.theaccountid + | ), + | (select + | string_agg( + | 'type:' + | || mappedaccountattribute.mtype + | ||',code:' + | ||mappedaccountattribute.mcode + | ||',value:' + | ||mappedaccountattribute.mvalue, + | ',') as account_attributes + | from mappedaccountattribute + | where + | mappedaccountattribute.maccountid = mappedbankaccount.theaccountid + | ) + |from mappedbankaccount + | LEFT JOIN mapperaccountholders + | ON (mappedbankaccount.bank = mapperaccountholders.accountbankpermalink and mappedbankaccount.theaccountid = mapperaccountholders.accountpermalink); + |CREATE INDEX account_id ON mv_fast_firehose_accounts(account_id); + |CREATE INDEX bank_id ON mv_fast_firehose_accounts(bank_id); + |""".stripMargin + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${ProductFee._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 32d945ecd..a3721b72f 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -843,9 +843,9 @@ object LocalMappedConnector extends Connector with MdcLoggable { val firehoseAccounts = { scalikeDB readOnly { implicit session => val sqlResult = sql""" - select * from v_fast_firehose_accounts - WHERE v_fast_firehose_accounts.bank_id = ${bankId.value} - ORDER BY v_fast_firehose_accounts.account_id $ordering + select * from mv_fast_firehose_accounts + WHERE mv_fast_firehose_accounts.bank_id = ${bankId.value} + ORDER BY mv_fast_firehose_accounts.account_id $ordering LIMIT $limit OFFSET $offset """.stripMargin From 1b9521b2a3f491ecb0de00f9e54085b6b4ddfb65 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 19 Oct 2021 14:29:45 +0200 Subject: [PATCH 078/185] test/fixed the failed test --- ...rationOfFastFireHoseMaterializedView.scala | 165 ++++++++++++------ 1 file changed, 111 insertions(+), 54 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala index 9ee9fb6cc..8cfa8adf1 100644 --- a/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala @@ -3,6 +3,7 @@ package code.api.util.migration import code.api.util.APIUtil import code.api.util.migration.Migration.{DbFunction, saveLog} import code.productfee.ProductFee +import net.liftweb.common.Full import net.liftweb.mapper.{DB, Schemifier} import net.liftweb.util.DefaultConnectionIdentifier @@ -24,60 +25,116 @@ object MigrationOfFastFireHoseMaterializedView { val executedSql = DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { - () => - """ - |CREATE MATERIALIZED VIEW mv_fast_firehose_accounts AS select - | mappedbankaccount.theaccountid as account_id, - | mappedbankaccount.bank as bank_id, - | mappedbankaccount.accountlabel as account_label, - | mappedbankaccount.accountnumber as account_number, - | (select - | string_agg( - | 'user_id:' - | || resourceuser.userid_ - | ||',provider:' - | ||resourceuser.provider_ - | ||',user_name:' - | ||resourceuser.name_, - | ',') as owners - | from resourceuser - | where - | resourceuser.id = mapperaccountholders.user_c - | ), - | mappedbankaccount.kind as kind, - | mappedbankaccount.accountcurrency as account_currency , - | mappedbankaccount.accountbalance as account_balance, - | (select - | string_agg( - | 'bank_id:' - | ||bankaccountrouting.bankid - | ||',account_id:' - | ||bankaccountrouting.accountid, - | ',' - | ) as account_routings - | from bankaccountrouting - | where - | bankaccountrouting.accountid = mappedbankaccount.theaccountid - | ), - | (select - | string_agg( - | 'type:' - | || mappedaccountattribute.mtype - | ||',code:' - | ||mappedaccountattribute.mcode - | ||',value:' - | ||mappedaccountattribute.mvalue, - | ',') as account_attributes - | from mappedaccountattribute - | where - | mappedaccountattribute.maccountid = mappedbankaccount.theaccountid - | ) - |from mappedbankaccount - | LEFT JOIN mapperaccountholders - | ON (mappedbankaccount.bank = mapperaccountholders.accountbankpermalink and mappedbankaccount.theaccountid = mapperaccountholders.accountpermalink); - |CREATE INDEX account_id ON mv_fast_firehose_accounts(account_id); - |CREATE INDEX bank_id ON mv_fast_firehose_accounts(bank_id); - |""".stripMargin + APIUtil.getPropsValue("db.driver") openOr("org.h2.Driver") match { + case value if value.contains("org.h2.Driver") => + () => //Note: H2 database, do not support the MATERIALIZED view + """ + |CREATE VIEW mv_fast_firehose_accounts AS select + | mappedbankaccount.theaccountid as account_id, + | mappedbankaccount.bank as bank_id, + | mappedbankaccount.accountlabel as account_label, + | mappedbankaccount.accountnumber as account_number, + | (select + | string_agg( + | 'user_id:' + | || resourceuser.userid_ + | ||',provider:' + | ||resourceuser.provider_ + | ||',user_name:' + | ||resourceuser.name_, + | ',') as owners + | from resourceuser + | where + | resourceuser.id = mapperaccountholders.user_c + | ), + | mappedbankaccount.kind as kind, + | mappedbankaccount.accountcurrency as account_currency , + | mappedbankaccount.accountbalance as account_balance, + | (select + | string_agg( + | 'bank_id:' + | ||bankaccountrouting.bankid + | ||',account_id:' + | ||bankaccountrouting.accountid, + | ',' + | ) as account_routings + | from bankaccountrouting + | where + | bankaccountrouting.accountid = mappedbankaccount.theaccountid + | ), + | (select + | string_agg( + | 'type:' + | || mappedaccountattribute.mtype + | ||',code:' + | ||mappedaccountattribute.mcode + | ||',value:' + | ||mappedaccountattribute.mvalue, + | ',') as account_attributes + | from mappedaccountattribute + | where + | mappedaccountattribute.maccountid = mappedbankaccount.theaccountid + | ) + |from mappedbankaccount + | LEFT JOIN mapperaccountholders + | ON (mappedbankaccount.bank = mapperaccountholders.accountbankpermalink and mappedbankaccount.theaccountid = mapperaccountholders.accountpermalink); + |""".stripMargin + case _ => + () => + """ + |CREATE MATERIALIZED VIEW mv_fast_firehose_accounts AS select + | mappedbankaccount.theaccountid as account_id, + | mappedbankaccount.bank as bank_id, + | mappedbankaccount.accountlabel as account_label, + | mappedbankaccount.accountnumber as account_number, + | (select + | string_agg( + | 'user_id:' + | || resourceuser.userid_ + | ||',provider:' + | ||resourceuser.provider_ + | ||',user_name:' + | ||resourceuser.name_, + | ',') as owners + | from resourceuser + | where + | resourceuser.id = mapperaccountholders.user_c + | ), + | mappedbankaccount.kind as kind, + | mappedbankaccount.accountcurrency as account_currency , + | mappedbankaccount.accountbalance as account_balance, + | (select + | string_agg( + | 'bank_id:' + | ||bankaccountrouting.bankid + | ||',account_id:' + | ||bankaccountrouting.accountid, + | ',' + | ) as account_routings + | from bankaccountrouting + | where + | bankaccountrouting.accountid = mappedbankaccount.theaccountid + | ), + | (select + | string_agg( + | 'type:' + | || mappedaccountattribute.mtype + | ||',code:' + | ||mappedaccountattribute.mcode + | ||',value:' + | ||mappedaccountattribute.mvalue, + | ',') as account_attributes + | from mappedaccountattribute + | where + | mappedaccountattribute.maccountid = mappedbankaccount.theaccountid + | ) + |from mappedbankaccount + | LEFT JOIN mapperaccountholders + | ON (mappedbankaccount.bank = mapperaccountholders.accountbankpermalink and mappedbankaccount.theaccountid = mapperaccountholders.accountpermalink); + |CREATE INDEX account_id ON mv_fast_firehose_accounts(account_id); + |CREATE INDEX bank_id ON mv_fast_firehose_accounts(bank_id); + |""".stripMargin + } } val endDate = System.currentTimeMillis() From f270ff56997e7d14c25c6203bc21eee50de93a8d Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 20 Oct 2021 10:00:17 +0200 Subject: [PATCH 079/185] refactor/removed the duplicate migration code --- ...rationOfFastFireHoseMaterializedView.scala | 159 ++++++------------ 1 file changed, 53 insertions(+), 106 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala index 8cfa8adf1..8184288b7 100644 --- a/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfFastFireHoseMaterializedView.scala @@ -23,117 +23,64 @@ object MigrationOfFastFireHoseMaterializedView { val commitId: String = APIUtil.gitCommit var isSuccessful = false + def migrationSql(isMaterializedView:Boolean) =s""" + |CREATE ${if(isMaterializedView) "MATERIALIZED" else ""} VIEW mv_fast_firehose_accounts AS select + | mappedbankaccount.theaccountid as account_id, + | mappedbankaccount.bank as bank_id, + | mappedbankaccount.accountlabel as account_label, + | mappedbankaccount.accountnumber as account_number, + | (select + | string_agg( + | 'user_id:' + | || resourceuser.userid_ + | ||',provider:' + | ||resourceuser.provider_ + | ||',user_name:' + | ||resourceuser.name_, + | ',') as owners + | from resourceuser + | where + | resourceuser.id = mapperaccountholders.user_c + | ), + | mappedbankaccount.kind as kind, + | mappedbankaccount.accountcurrency as account_currency , + | mappedbankaccount.accountbalance as account_balance, + | (select + | string_agg( + | 'bank_id:' + | ||bankaccountrouting.bankid + | ||',account_id:' + | ||bankaccountrouting.accountid, + | ',' + | ) as account_routings + | from bankaccountrouting + | where + | bankaccountrouting.accountid = mappedbankaccount.theaccountid + | ), + | (select + | string_agg( + | 'type:' + | || mappedaccountattribute.mtype + | ||',code:' + | ||mappedaccountattribute.mcode + | ||',value:' + | ||mappedaccountattribute.mvalue, + | ',') as account_attributes + | from mappedaccountattribute + | where + | mappedaccountattribute.maccountid = mappedbankaccount.theaccountid + | ) + |from mappedbankaccount + | LEFT JOIN mapperaccountholders + | ON (mappedbankaccount.bank = mapperaccountholders.accountbankpermalink and mappedbankaccount.theaccountid = mapperaccountholders.accountpermalink); + |""".stripMargin val executedSql = DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { APIUtil.getPropsValue("db.driver") openOr("org.h2.Driver") match { case value if value.contains("org.h2.Driver") => - () => //Note: H2 database, do not support the MATERIALIZED view - """ - |CREATE VIEW mv_fast_firehose_accounts AS select - | mappedbankaccount.theaccountid as account_id, - | mappedbankaccount.bank as bank_id, - | mappedbankaccount.accountlabel as account_label, - | mappedbankaccount.accountnumber as account_number, - | (select - | string_agg( - | 'user_id:' - | || resourceuser.userid_ - | ||',provider:' - | ||resourceuser.provider_ - | ||',user_name:' - | ||resourceuser.name_, - | ',') as owners - | from resourceuser - | where - | resourceuser.id = mapperaccountholders.user_c - | ), - | mappedbankaccount.kind as kind, - | mappedbankaccount.accountcurrency as account_currency , - | mappedbankaccount.accountbalance as account_balance, - | (select - | string_agg( - | 'bank_id:' - | ||bankaccountrouting.bankid - | ||',account_id:' - | ||bankaccountrouting.accountid, - | ',' - | ) as account_routings - | from bankaccountrouting - | where - | bankaccountrouting.accountid = mappedbankaccount.theaccountid - | ), - | (select - | string_agg( - | 'type:' - | || mappedaccountattribute.mtype - | ||',code:' - | ||mappedaccountattribute.mcode - | ||',value:' - | ||mappedaccountattribute.mvalue, - | ',') as account_attributes - | from mappedaccountattribute - | where - | mappedaccountattribute.maccountid = mappedbankaccount.theaccountid - | ) - |from mappedbankaccount - | LEFT JOIN mapperaccountholders - | ON (mappedbankaccount.bank = mapperaccountholders.accountbankpermalink and mappedbankaccount.theaccountid = mapperaccountholders.accountpermalink); - |""".stripMargin + () => migrationSql(false)//Note: H2 database, do not support the MATERIALIZED view case _ => - () => - """ - |CREATE MATERIALIZED VIEW mv_fast_firehose_accounts AS select - | mappedbankaccount.theaccountid as account_id, - | mappedbankaccount.bank as bank_id, - | mappedbankaccount.accountlabel as account_label, - | mappedbankaccount.accountnumber as account_number, - | (select - | string_agg( - | 'user_id:' - | || resourceuser.userid_ - | ||',provider:' - | ||resourceuser.provider_ - | ||',user_name:' - | ||resourceuser.name_, - | ',') as owners - | from resourceuser - | where - | resourceuser.id = mapperaccountholders.user_c - | ), - | mappedbankaccount.kind as kind, - | mappedbankaccount.accountcurrency as account_currency , - | mappedbankaccount.accountbalance as account_balance, - | (select - | string_agg( - | 'bank_id:' - | ||bankaccountrouting.bankid - | ||',account_id:' - | ||bankaccountrouting.accountid, - | ',' - | ) as account_routings - | from bankaccountrouting - | where - | bankaccountrouting.accountid = mappedbankaccount.theaccountid - | ), - | (select - | string_agg( - | 'type:' - | || mappedaccountattribute.mtype - | ||',code:' - | ||mappedaccountattribute.mcode - | ||',value:' - | ||mappedaccountattribute.mvalue, - | ',') as account_attributes - | from mappedaccountattribute - | where - | mappedaccountattribute.maccountid = mappedbankaccount.theaccountid - | ) - |from mappedbankaccount - | LEFT JOIN mapperaccountholders - | ON (mappedbankaccount.bank = mapperaccountholders.accountbankpermalink and mappedbankaccount.theaccountid = mapperaccountholders.accountpermalink); - |CREATE INDEX account_id ON mv_fast_firehose_accounts(account_id); - |CREATE INDEX bank_id ON mv_fast_firehose_accounts(bank_id); - |""".stripMargin + () => migrationSql(true) } } From 8e4dabaf924f61b38db2386f6ee83427761091cf Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 20 Oct 2021 11:01:04 +0200 Subject: [PATCH 080/185] refactor/remove the role error message CreateUserAuthContextError --- obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index 2fa88b736..a33571670 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -1450,7 +1450,6 @@ trait APIMethods310 { List( UserNotLoggedIn, UserHasMissingRoles, - CreateUserAuthContextError, UnknownError ), List(apiTagUser, apiTagNewStyle), From fb6d8d8be568103f05f6a67f07bae59e2a25d273 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 21 Oct 2021 13:36:48 +0200 Subject: [PATCH 081/185] docfix/tweaked the dynamic entity and endpoint glossary --- obp-api/src/main/scala/code/api/util/Glossary.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2bd3a303e..7a6ea4f12 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2188,7 +2188,7 @@ object Glossary extends MdcLoggable { glossaryItems += GlossaryItem( - title = "Dynamic Entity", + title = "Dynamic Entity Manage", description = s""" | @@ -2239,7 +2239,7 @@ object Glossary extends MdcLoggable { """.stripMargin) glossaryItems += GlossaryItem( - title = "Dynamic Endpoint", + title = "Dynamic Endpoint Manage", description = s""" | From 170726f6c5f01c4a8e149b9df68289a7bcd5bf7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 21 Oct 2021 14:50:02 +0200 Subject: [PATCH 082/185] feature/Add props: openid_connect.response_type, openid_connect.scope and openid_connect.response_mode --- .../resources/props/sample.props.template | 11 ++- .../code/snippet/OpenidConnectInvoke.scala | 71 +++++++++++++++---- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 8ea3dc84f..dba483529 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -628,6 +628,15 @@ super_admin_user_ids=USER_ID1,USER_ID2, ## registered on OBP localy. # openid_connect.enabled=true # openid_connect.show_tokens=false +# Response mode +# possible values: query, fragment, form_post, query.jwt, fragment.jwt, form_post.jwt, jwt +# openid_connect.response_mode=form_post +# Response type +# possible values: "code", "id_token", "code id_token" +# openid_connect.response_type=code +# Scope +# possible values: "openid email profile", "openid email", "openid" +# openid_connect.scope=openid email profile # First identity provider # openid_connect_1.button_text = Google # openid_connect_1.client_secret=OYdWujJlU7fFOW_NXzPlDI4T @@ -642,7 +651,7 @@ super_admin_user_ids=USER_ID1,USER_ID2, # openid_connect_2.button_text = name of 2nd provider # openid_connect_2.client_secret=OYdWujJlU7fFOW_NXzPlDI4T # openid_connect_2.client_id=883773244832-s4hi72j0rble0iiivq1gn09k7vvptdci.apps.googleusercontent.com -# openid_connect_2.callback_url=http://127.0.0.1:8080/auth/openid-connect/callback +# openid_connect_2.callback_url=http://127.0.0.1:8080/auth/openid-connect/callback-2 # openid_connect_2.endpoint.authorization=https://accounts.google.com/o/oauth2/v2/auth # openid_connect_2.endpoint.userinfo=https://openidconnect.googleapis.com/v1/userinfo # openid_connect_2.endpoint.token=https://oauth2.googleapis.com/token diff --git a/obp-api/src/main/scala/code/snippet/OpenidConnectInvoke.scala b/obp-api/src/main/scala/code/snippet/OpenidConnectInvoke.scala index 9429a8088..cd08a06ef 100644 --- a/obp-api/src/main/scala/code/snippet/OpenidConnectInvoke.scala +++ b/obp-api/src/main/scala/code/snippet/OpenidConnectInvoke.scala @@ -3,9 +3,12 @@ package code.snippet import java.net.URI import code.api.OpenIdConnectConfig +import code.api.util.APIUtil +import com.nimbusds.jwt.JWT import com.nimbusds.oauth2.sdk.id.{ClientID, State} -import com.nimbusds.oauth2.sdk.{ResponseType, Scope} -import com.nimbusds.openid.connect.sdk.{AuthenticationRequest, Nonce} +import com.nimbusds.oauth2.sdk.pkce.{CodeChallenge, CodeChallengeMethod} +import com.nimbusds.oauth2.sdk.{ResponseMode, ResponseType, Scope} +import com.nimbusds.openid.connect.sdk.{AuthenticationRequest, Display, Nonce, OIDCClaimsRequest, OIDCScopeValue, Prompt} import net.liftweb.common.{Box, Empty, Full, Loggable} import net.liftweb.http.js.{JsCmd, JsCmds} import net.liftweb.http.{S, SHtml, SessionVar} @@ -36,17 +39,61 @@ object OpenidConnectInvoke extends Loggable { // Generate nonce val nonce = new Nonce() + val responseMode = APIUtil.getPropsValue("openid_connect.response_mode", "form_post") match { + case "query" => ResponseMode.QUERY + case "fragment" => ResponseMode.FRAGMENT + case "form_post" => ResponseMode.FORM_POST + case "query.jwt" => ResponseMode.QUERY_JWT + case "fragment.jwt" => ResponseMode.FRAGMENT_JWT + case "form_post.jwt" => ResponseMode.FORM_POST_JWT + case "jwt" => ResponseMode.JWT + case _ => ResponseMode.FORM_POST + } + val responseType = APIUtil.getPropsValue("openid_connect.response_type", "code") match { + case "code" => new ResponseType("code") + case "id_token" => new ResponseType("id_token") + case "code id_token" => new ResponseType("code", "id_token") + case _ => new ResponseType("code") + } + val scope = APIUtil.getPropsValue("openid_connect.scope", "openid email profile") match { + case "openid email profile" => + val scope: Scope = new Scope(); + scope.add(OIDCScopeValue.OPENID); + scope.add(OIDCScopeValue.EMAIL); + scope.add(OIDCScopeValue.PROFILE); + scope + case "openid email" => + val scope: Scope = new Scope(); + scope.add(OIDCScopeValue.OPENID); + scope.add(OIDCScopeValue.EMAIL); + scope + case "openid" => + val scope: Scope = new Scope(); + scope.add(OIDCScopeValue.OPENID); + scope + case _ => + val scope: Scope = new Scope(); + scope.add(OIDCScopeValue.OPENID); + scope.add(OIDCScopeValue.EMAIL); + scope.add(OIDCScopeValue.PROFILE); + scope + } + // Compose the request (in code flow) - val req = - new AuthenticationRequest( - new URI(OpenIdConnectConfig.get(identityProvider).authorization_endpoint), - new ResponseType("code"), - Scope.parse("openid email profile"), - clientID, - callback, - state, - nonce - ) + val req = new AuthenticationRequest( + new URI(OpenIdConnectConfig.get(identityProvider).authorization_endpoint), + responseType, + responseMode, + scope, + clientID, + callback, + state, + nonce, + null, null, -1, null, null, + null, null, null, null.asInstanceOf[OIDCClaimsRequest], null, + null, null, + null, null, + null, false, null) OpenIDConnectSessionState.set(Full(state)) val accessType = if (OpenIdConnectConfig.get(identityProvider).access_type_offline) "&access_type=offline" else "" val redirectTo = req.toHTTPRequest.getURL() + "?" + req.toHTTPRequest.getQuery() + accessType From 4bb320e0a92aef333b042913d46c29b48fea803c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 25 Oct 2021 13:15:13 +0200 Subject: [PATCH 083/185] feature/Improve modul RSAUtil --- .../main/scala/code/api/util/RSAUtil.scala | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/RSAUtil.scala b/obp-api/src/main/scala/code/api/util/RSAUtil.scala index e8baabb9d..f2c2b55ed 100644 --- a/obp-api/src/main/scala/code/api/util/RSAUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RSAUtil.scala @@ -2,7 +2,9 @@ package code.api.util import code.api.util.CertificateUtil.{privateKey, publicKey} import code.util.Helper.MdcLoggable +import com.nimbusds.jose.{JWSAlgorithm, JWSHeader, JWSObject, Payload} import javax.crypto.Cipher +import net.liftweb.util.SecurityHelpers object RSAUtil extends MdcLoggable { @@ -16,20 +18,59 @@ object RSAUtil extends MdcLoggable { Base64.encodeBase64String(res) } def decrypt(encrypted: String): String = { - import org.apache.commons.codec.binary.Base64 import javax.crypto.Cipher + import org.apache.commons.codec.binary.Base64 val bytes = Base64.decodeBase64(encrypted) val cipher = Cipher.getInstance(cryptoSystem) cipher.init(Cipher.DECRYPT_MODE, privateKey) new String(cipher.doFinal(bytes), "utf-8") } + def computeHash(input: String): String = SecurityHelpers.hash256(input) + def computeHexHash(input: String): String = { + SecurityHelpers.hexDigest256(input.getBytes("UTF-8")) + } + + def signWithRsa256(payload: String): String = { + // Prepare JWS object with simple string as payload + val jwsObject = new JWSObject( + new JWSHeader.Builder(JWSAlgorithm.RS256).build, + new Payload(payload) + ) + // Compute the RSA signature + jwsObject.sign(CertificateUtil.rsaSigner) + + // To serialize to compact form, produces something like + // eyJhbGciOiJSUzI1NiJ9.SW4gUlNBIHdlIHRydXN0IQ.IRMQENi4nJyp4er2L + // mZq3ivwoAjqa1uUkSBKFIX7ATndFF5ivnt-m8uApHO4kfIFOrW7w2Ezmlg3Qd + // maXlS9DhN0nUk_hGI3amEjkKd0BWYCB8vfUbUv0XGjQip78AI4z1PrFRNidm7 + // -jPDm5Iq0SZnjKjCNS5Q15fokXZc8u0A + val s = jwsObject.serialize + s + } + + def computeXSign(input: String) = { + logger.debug("Input: " + input) + logger.debug("Hash: " + computeHash(input)) + logger.debug("HEX hash: " + computeHexHash(input)) + // Compute JWS token + val jws = signWithRsa256(computeHexHash(input)) + logger.debug("RSA 256 signature: " + jws) + // Get the last i.e. 3rd part of JWS token + val xSign = jws.split('.').toList.last + logger.debug("x-sign: " + xSign) + xSign + } + def main(args: Array[String]): Unit = { val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=f" val res = encrypt(db) println("db.url: " + db) println("encrypt: " + res) println("decrypt: " + decrypt(res)) + + val inputMessage = """hello world\n""" + computeXSign(inputMessage) } } From b6389d1a44fc15ad37e7382e8d8ccb2592cacb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 25 Oct 2021 19:25:39 +0200 Subject: [PATCH 084/185] feature/Improve modul RSAUtil 2 --- .../main/scala/code/api/util/RSAUtil.scala | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/RSAUtil.scala b/obp-api/src/main/scala/code/api/util/RSAUtil.scala index f2c2b55ed..add1edc25 100644 --- a/obp-api/src/main/scala/code/api/util/RSAUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RSAUtil.scala @@ -1,7 +1,11 @@ package code.api.util +import java.nio.file.{Files, Paths} + import code.api.util.CertificateUtil.{privateKey, publicKey} import code.util.Helper.MdcLoggable +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.{JWSAlgorithm, JWSHeader, JWSObject, Payload} import javax.crypto.Cipher import net.liftweb.util.SecurityHelpers @@ -31,14 +35,16 @@ object RSAUtil extends MdcLoggable { SecurityHelpers.hexDigest256(input.getBytes("UTF-8")) } - def signWithRsa256(payload: String): String = { - // Prepare JWS object with simple string as payload + def signWithRsa256(payload: String, jwk: JWK): String = { + // Prepare JWS object with simple string as a payload val jwsObject = new JWSObject( new JWSHeader.Builder(JWSAlgorithm.RS256).build, new Payload(payload) ) + + val rsaSigner = new RSASSASigner(jwk.toRSAKey) // Compute the RSA signature - jwsObject.sign(CertificateUtil.rsaSigner) + jwsObject.sign(rsaSigner) // To serialize to compact form, produces something like // eyJhbGciOiJSUzI1NiJ9.SW4gUlNBIHdlIHRydXN0IQ.IRMQENi4nJyp4er2L @@ -49,18 +55,28 @@ object RSAUtil extends MdcLoggable { s } - def computeXSign(input: String) = { + def computeXSign(input: String, jwk: JWK) = { logger.debug("Input: " + input) logger.debug("Hash: " + computeHash(input)) logger.debug("HEX hash: " + computeHexHash(input)) // Compute JWS token - val jws = signWithRsa256(computeHexHash(input)) + val jws = signWithRsa256(computeHexHash(input), jwk) logger.debug("RSA 256 signature: " + jws) // Get the last i.e. 3rd part of JWS token val xSign = jws.split('.').toList.last logger.debug("x-sign: " + xSign) xSign } + + def getPrivateKeyFromFile(path: String): JWK = { + val pathOfFile = Paths.get(path) + val pemEncodedRSAPrivateKey = Files.readAllLines(pathOfFile).toArray.toList.mkString("\n") + logger.debug(pemEncodedRSAPrivateKey) + // Parse PEM-encoded key to RSA public / private JWK + val jwk: JWK = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey); + logger.debug("Key is private: " + jwk.isPrivate) + jwk + } def main(args: Array[String]): Unit = { val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=f" @@ -69,8 +85,12 @@ object RSAUtil extends MdcLoggable { println("encrypt: " + res) println("decrypt: " + decrypt(res)) - val inputMessage = """hello world\n""" - computeXSign(inputMessage) + val timestamp = "1634805183" + val uri = "https://api.qredo.network/api/v1/p/company" + val body = """{"name":"Tesobe GmbH","city":"Berlin","country":"DE","domain":"tesobe.com","ref":"9827feec-4eae-4e80-bda3-daa7c3b97ad1"}""" + val inputMessage = s"""${timestamp}${uri}${body}""" + val privateKey = getPrivateKeyFromFile("obp-api/src/test/resources/cert/private.pem") + computeXSign(inputMessage, privateKey) } } From 0f9f26616b2004ebc0a65eb74556174b36987bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 26 Oct 2021 11:03:49 +0200 Subject: [PATCH 085/185] feature/Improve modul RSAUtil 3 --- .../src/main/scala/code/api/util/RSAUtil.scala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/RSAUtil.scala b/obp-api/src/main/scala/code/api/util/RSAUtil.scala index add1edc25..46ce76ae4 100644 --- a/obp-api/src/main/scala/code/api/util/RSAUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RSAUtil.scala @@ -1,12 +1,13 @@ package code.api.util import java.nio.file.{Files, Paths} +import java.security.Signature import code.api.util.CertificateUtil.{privateKey, publicKey} import code.util.Helper.MdcLoggable import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.{JWSAlgorithm, JWSHeader, JWSObject, Payload} +import com.nimbusds.jose.{JOSEObjectType, JWSAlgorithm, JWSHeader, JWSObject, Payload} import javax.crypto.Cipher import net.liftweb.util.SecurityHelpers @@ -59,11 +60,14 @@ object RSAUtil extends MdcLoggable { logger.debug("Input: " + input) logger.debug("Hash: " + computeHash(input)) logger.debug("HEX hash: " + computeHexHash(input)) - // Compute JWS token - val jws = signWithRsa256(computeHexHash(input), jwk) - logger.debug("RSA 256 signature: " + jws) - // Get the last i.e. 3rd part of JWS token - val xSign = jws.split('.').toList.last + // Compute the signature + import sun.misc.BASE64Encoder + val data = input.getBytes("UTF8") + val sig = Signature.getInstance("SHA256WithRSA") + sig.initSign(jwk.toRSAKey.toPrivateKey) + sig.update(data) + val signatureBytes = sig.sign + val xSign = new BASE64Encoder().encode(signatureBytes) logger.debug("x-sign: " + xSign) xSign } @@ -91,6 +95,8 @@ object RSAUtil extends MdcLoggable { val inputMessage = s"""${timestamp}${uri}${body}""" val privateKey = getPrivateKeyFromFile("obp-api/src/test/resources/cert/private.pem") computeXSign(inputMessage, privateKey) + + } } From 10a0e4a48c3c80989bf8b8a4ebaeed83b4c2f468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 26 Oct 2021 12:21:16 +0200 Subject: [PATCH 086/185] feature/Improve modul RSAUtil 4 --- obp-api/src/main/scala/code/api/util/RSAUtil.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/RSAUtil.scala b/obp-api/src/main/scala/code/api/util/RSAUtil.scala index 46ce76ae4..777c7f5e0 100644 --- a/obp-api/src/main/scala/code/api/util/RSAUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RSAUtil.scala @@ -7,9 +7,10 @@ import code.api.util.CertificateUtil.{privateKey, publicKey} import code.util.Helper.MdcLoggable import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.{JOSEObjectType, JWSAlgorithm, JWSHeader, JWSObject, Payload} +import com.nimbusds.jose.{JWSAlgorithm, JWSHeader, JWSObject, Payload} import javax.crypto.Cipher import net.liftweb.util.SecurityHelpers +import net.liftweb.util.SecurityHelpers.base64EncodeURLSafe object RSAUtil extends MdcLoggable { @@ -61,13 +62,12 @@ object RSAUtil extends MdcLoggable { logger.debug("Hash: " + computeHash(input)) logger.debug("HEX hash: " + computeHexHash(input)) // Compute the signature - import sun.misc.BASE64Encoder val data = input.getBytes("UTF8") val sig = Signature.getInstance("SHA256WithRSA") sig.initSign(jwk.toRSAKey.toPrivateKey) sig.update(data) val signatureBytes = sig.sign - val xSign = new BASE64Encoder().encode(signatureBytes) + val xSign = base64EncodeURLSafe(signatureBytes) logger.debug("x-sign: " + xSign) xSign } From ccefe6dc20051960dd059e1c7c1dfd27812cf375 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 26 Oct 2021 12:35:02 +0200 Subject: [PATCH 087/185] feature/added the timestamp for the RSAUtil test --- obp-api/src/main/scala/code/api/util/RSAUtil.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/RSAUtil.scala b/obp-api/src/main/scala/code/api/util/RSAUtil.scala index 777c7f5e0..db566dff9 100644 --- a/obp-api/src/main/scala/code/api/util/RSAUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RSAUtil.scala @@ -2,7 +2,6 @@ package code.api.util import java.nio.file.{Files, Paths} import java.security.Signature - import code.api.util.CertificateUtil.{privateKey, publicKey} import code.util.Helper.MdcLoggable import com.nimbusds.jose.crypto.RSASSASigner @@ -11,6 +10,7 @@ import com.nimbusds.jose.{JWSAlgorithm, JWSHeader, JWSObject, Payload} import javax.crypto.Cipher import net.liftweb.util.SecurityHelpers import net.liftweb.util.SecurityHelpers.base64EncodeURLSafe +import java.time.Instant object RSAUtil extends MdcLoggable { @@ -89,12 +89,13 @@ object RSAUtil extends MdcLoggable { println("encrypt: " + res) println("decrypt: " + decrypt(res)) - val timestamp = "1634805183" + val timestamp = Instant.now.getEpochSecond val uri = "https://api.qredo.network/api/v1/p/company" val body = """{"name":"Tesobe GmbH","city":"Berlin","country":"DE","domain":"tesobe.com","ref":"9827feec-4eae-4e80-bda3-daa7c3b97ad1"}""" val inputMessage = s"""${timestamp}${uri}${body}""" val privateKey = getPrivateKeyFromFile("obp-api/src/test/resources/cert/private.pem") computeXSign(inputMessage, privateKey) + logger.debug("timestamp: " + timestamp) } From 19d93bde9b125691aed1875dfe74710d9ded4901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 26 Oct 2021 14:28:44 +0200 Subject: [PATCH 088/185] docfix/Tweak i18n props invalid.username --- obp-api/src/main/resources/i18n/lift-core.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/resources/i18n/lift-core.properties b/obp-api/src/main/resources/i18n/lift-core.properties index eceed845a..d9bc9055e 100644 --- a/obp-api/src/main/resources/i18n/lift-core.properties +++ b/obp-api/src/main/resources/i18n/lift-core.properties @@ -372,7 +372,8 @@ invalid.username=Invalid Username: \ 2) Usernames MUST be between 8 and 100 characters long \ 3) Usernames MUST NOT start with _ or . \ 4) Usernames MUST NOT contain __ or ._ or ._ or .. \ -5) Usernames MUST NOT end with _ or . +5) Usernames MUST NOT end with _ or . \ +6) Any valid email address is allowed as the Username your.username.is.not.unique = Your username is not unique. Please enter a different one. # Those 2 messages must have the same output in order to prevent leakage of information From 764303134a21aa1484f85b0b2055b51f57a4f0fa Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 26 Oct 2021 15:12:32 +0200 Subject: [PATCH 089/185] feature/added the getPrivateKeyFromString method --- obp-api/src/main/scala/code/api/util/RSAUtil.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/obp-api/src/main/scala/code/api/util/RSAUtil.scala b/obp-api/src/main/scala/code/api/util/RSAUtil.scala index db566dff9..f8aefefd8 100644 --- a/obp-api/src/main/scala/code/api/util/RSAUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RSAUtil.scala @@ -82,6 +82,15 @@ object RSAUtil extends MdcLoggable { jwk } + def getPrivateKeyFromString(privateKeyValue: String): JWK = { + val pemEncodedRSAPrivateKey = privateKeyValue + logger.debug(privateKeyValue) + // Parse PEM-encoded key to RSA public / private JWK + val jwk: JWK = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey); + logger.debug("Key is private: " + jwk.isPrivate) + jwk + } + def main(args: Array[String]): Unit = { val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=f" val res = encrypt(db) From fba4a0ffe653fcdf59649790642cb775b5abff6b Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 26 Oct 2021 15:32:41 +0200 Subject: [PATCH 090/185] feature/added the missing headers --- .../rest/RestConnector_vMar2019.scala | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index 0e9d6b9eb..7f2146ebd 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -37,6 +37,7 @@ import code.api.cache.Caching import code.api.util.APIUtil.{AdapterImplementation, MessageDoc, OBPReturnType, saveConnectorMetric, _} import code.api.util.ErrorMessages._ import code.api.util.ExampleValue._ +import code.api.util.RSAUtil.{computeXSign, getPrivateKeyFromString} import code.api.util.{APIUtil, CallContext, OBPQueryParam} import code.api.v4_0_0.dynamic.MockResponseHolder import code.bankconnectors._ @@ -63,6 +64,7 @@ import net.liftweb.json.{JValue, _} import net.liftweb.util.Helpers.tryo import org.apache.commons.lang3.StringUtils +import java.time.Instant import scala.collection.immutable.List import scala.collection.mutable.ArrayBuffer import scala.concurrent.duration._ @@ -6522,7 +6524,7 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable } val jsonToSend = if(jValue == JNothing) "" else compactRender(jValue) - val request = prepareHttpRequest(paramUrl, method, HttpProtocol("HTTP/1.1"), jsonToSend).withHeaders(callContext) + val request = prepareHttpRequest(paramUrl, method, HttpProtocol("HTTP/1.1"), jsonToSend).withHeaders(buildHeaders(paramUrl,jsonToSend,callContext)) logger.debug(s"RestConnector_vMar2019 request is : $request") val responseFuture = makeHttpRequest(request) @@ -6572,15 +6574,31 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable //In RestConnector, we use the headers to propagate the parameters to Adapter. The parameters come from the CallContext.outboundAdapterAuthInfo.userAuthContext //We can set them from UserOauthContext or the http request headers. - private[this] implicit def buildHeaders(callContext: Option[CallContext]): List[HttpHeader] = { + private[this] def buildHeaders( + uri: String, + entityJsonString: String, + callContext: Option[CallContext] + ): List[HttpHeader] = { - val generalContext = callContext.flatMap(_.toOutboundAdapterCallContext.generalContext).getOrElse(List.empty[BasicGeneralContext]) - val headersFromGeneralContext = generalContext.map(generalContext => RawHeader(generalContext.key,generalContext.value)) +// val generalContext = callContext.flatMap(_.toOutboundAdapterCallContext.generalContext).getOrElse(List.empty[BasicGeneralContext]) +// val headersFromGeneralContext = generalContext.map(generalContext => RawHeader(generalContext.key,generalContext.value)) val basicUserAuthContexts: List[BasicUserAuthContext] = callContext.flatMap(_.toOutboundAdapterCallContext.outboundAdapterAuthInfo.flatMap(_.userAuthContext)).getOrElse(List.empty[BasicUserAuthContext]) - val headersFromUserAuthContext = basicUserAuthContexts.map(userAuthContext => RawHeader(userAuthContext.key,userAuthContext.value)) + val headersFromUserAuthContext = basicUserAuthContexts.filterNot(_.key == "private-key").map(userAuthContext =>RawHeader(userAuthContext.key,userAuthContext.value)) + + val timeStamp = Instant.now.getEpochSecond.toString + val inputMessage = s"""${timeStamp}${uri}${entityJsonString}""" + val privateKeyValue = basicUserAuthContexts.find(_.key =="private-key").map(_.value).getOrElse("") + val privateKey = getPrivateKeyFromString(privateKeyValue) + + val xSign = computeXSign(inputMessage, privateKey) + val extraHeaders = List(RawHeader("x-timestamp",timeStamp),RawHeader("x-sign",xSign)) + val headers = headersFromUserAuthContext++extraHeaders + logger.debug(s"x-timestamp: $timeStamp") + logger.debug(s"x-sign: $xSign") + logger.debug(s"obp headers: ${headers}") - headersFromGeneralContext++headersFromUserAuthContext + headers } @@ -6686,7 +6704,7 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable compactRender(builtJson) case _ => net.liftweb.json.Serialization.write(outBound) } - val request = prepareHttpRequest(url, method, HttpProtocol("HTTP/1.1"), outBoundJson).withHeaders(callContext) + val request = prepareHttpRequest(url, method, HttpProtocol("HTTP/1.1"), outBoundJson).withHeaders(buildHeaders(url, outBoundJson, callContext)) logger.debug(s"RestConnector_vMar2019 request is : $request") val responseFuture = makeHttpRequest(request) responseFuture.map { From 58b6f6b8b6389d112b08af8209975df63edf42b9 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 27 Oct 2021 17:18:29 +0200 Subject: [PATCH 091/185] /docfix: #authUser.skipEmailValidation=false --- obp-api/src/main/resources/props/sample.props.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 2cd0951af..89cc0181d 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -652,8 +652,8 @@ consumers_enabled_by_default=true # Autocomplete for login form has to be explicitly set autocomplete_at_login_form_enabled=false -# Skip Auth User Email validation (defaults to true) -#authUser.skipEmailValidation=true +# Skip Auth User Email validation (defaults to false as of 29 June 2021) +#authUser.skipEmailValidation=false # If using Kafka but want to get counterparties from OBP, set this to true #get_counterparties_from_OBP_DB=true From 03305d2be18053f066e5bb06e8ae35dc6526534e Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 1 Nov 2021 14:23:53 +0100 Subject: [PATCH 092/185] feature/added rest_connector_http_header_signature props --- .../rest/RestConnector_vMar2019.scala | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index 7f2146ebd..9411c2a8c 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -6580,22 +6580,28 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable callContext: Option[CallContext] ): List[HttpHeader] = { -// val generalContext = callContext.flatMap(_.toOutboundAdapterCallContext.generalContext).getOrElse(List.empty[BasicGeneralContext]) -// val headersFromGeneralContext = generalContext.map(generalContext => RawHeader(generalContext.key,generalContext.value)) + val needSignatureHead = APIUtil.getPropsAsBoolValue("rest_connector_http_header_signature", false) + val generalContext = callContext.flatMap(_.toOutboundAdapterCallContext.generalContext).getOrElse(List.empty[BasicGeneralContext]) + val headersFromGeneralContext = generalContext.map(generalContext => RawHeader(generalContext.key,generalContext.value)) val basicUserAuthContexts: List[BasicUserAuthContext] = callContext.flatMap(_.toOutboundAdapterCallContext.outboundAdapterAuthInfo.flatMap(_.userAuthContext)).getOrElse(List.empty[BasicUserAuthContext]) val headersFromUserAuthContext = basicUserAuthContexts.filterNot(_.key == "private-key").map(userAuthContext =>RawHeader(userAuthContext.key,userAuthContext.value)) - + val timeStamp = Instant.now.getEpochSecond.toString - val inputMessage = s"""${timeStamp}${uri}${entityJsonString}""" - val privateKeyValue = basicUserAuthContexts.find(_.key =="private-key").map(_.value).getOrElse("") - val privateKey = getPrivateKeyFromString(privateKeyValue) - - val xSign = computeXSign(inputMessage, privateKey) - val extraHeaders = List(RawHeader("x-timestamp",timeStamp),RawHeader("x-sign",xSign)) - val headers = headersFromUserAuthContext++extraHeaders logger.debug(s"x-timestamp: $timeStamp") - logger.debug(s"x-sign: $xSign") + + val extraHeaders = if(needSignatureHead){ + val inputMessage = s"""${timeStamp}${uri}${entityJsonString}""" + val privateKeyValue = basicUserAuthContexts.find(_.key =="private-key").map(_.value).getOrElse("") + val privateKey = getPrivateKeyFromString(privateKeyValue) + val xSign = computeXSign(inputMessage, privateKey) + logger.debug(s"x-sign: $xSign") + List(RawHeader("x-timestamp",timeStamp),RawHeader("x-sign",xSign)) + } else { + List(RawHeader("x-timestamp",timeStamp)) + } + val headers = headersFromUserAuthContext++extraHeaders++headersFromGeneralContext + logger.debug(s"obp headers: ${headers}") headers From 77039d71d60f5085a07e1717ce829f4813672dc0 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 1 Nov 2021 14:31:31 +0100 Subject: [PATCH 093/185] docfix/added rest_connector_http_header_signature to release_notes.md --- obp-api/src/main/resources/props/sample.props.template | 4 +++- release_notes.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index d8898b4bc..e3349524f 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -805,7 +805,9 @@ featured_apis=elasticSearchWarehouseV300 # If Rest Connector do not get the response in the following seconds, it will throw the error message back. # This props can be omitted, the default value is 59. It should be less than Nginx timeout. # rest2019_connector_timeout = 59 - +# If set it to `true`, it will add the signature (SHA256WithRSA) into each the rest connector http calls, and you need to upload the RSA +# private key into the UserAuthContext. +#rest_connector_http_header_signature=false # -- Scopes ----------------------------------------------------- diff --git a/release_notes.md b/release_notes.md index 4159cc333..612c6f8f8 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,6 +3,7 @@ ### Most recent changes at top of file ``` Date Commit Action +01/11/2021 03305d2b Added props: rest_connector_http_header_signature, default is false 17/09/2021 e65cd51d Added props: webui_main_faq_external_link, default is obp static file: /main-faq.html 09/09/2021 65952225 Added props: webui_support_email, default is contact@openbankproject.com 02/09/2021 a826d908 Renamed Web UI props: From 374d6e71c2bd89d6a7da0d1e325c1a13e249f740 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 1 Nov 2021 17:10:17 +0100 Subject: [PATCH 094/185] bugfix/fixed the GatewayLogin username issue --- obp-api/src/main/scala/code/api/GatewayLogin.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/GatewayLogin.scala b/obp-api/src/main/scala/code/api/GatewayLogin.scala index e8ae2823e..e66c33160 100755 --- a/obp-api/src/main/scala/code/api/GatewayLogin.scala +++ b/obp-api/src/main/scala/code/api/GatewayLogin.scala @@ -482,7 +482,7 @@ object GatewayLogin extends RestHelper with MdcLoggable { val payload = GatewayLogin.parseJwt(parameters) payload match { case Full(payload) => - val username = getFieldFromPayloadJson(payload, "username") + val username = getFieldFromPayloadJson(payload, "login_user_name") logger.debug("username: " + username) Users.users.vend.getUserByProviderId(provider = gateway, idGivenByProvider = username) case _ => From 0893997ae79572ab42eebb2eb83ec639c92e5925 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 1 Nov 2021 17:11:23 +0100 Subject: [PATCH 095/185] feature/added the new login type: DAuthLogin - step1 --- .../resources/props/sample.props.template | 10 + .../src/main/scala/code/api/DAuthLogin.scala | 333 ++++++++++++++++++ .../main/scala/code/api/OBPRestHelper.scala | 43 ++- .../main/scala/code/api/util/APIUtil.scala | 69 +++- .../main/scala/code/api/util/ApiSession.scala | 38 +- .../scala/code/api/util/ErrorMessages.scala | 11 + 6 files changed, 494 insertions(+), 10 deletions(-) create mode 100755 obp-api/src/main/scala/code/api/DAuthLogin.scala diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index e3349524f..28182aa96 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -688,6 +688,16 @@ autocomplete_at_login_form_enabled=false # gateway.token_secret=secret # -------------------------------------- Gateway login -- +# -- DAuth login -------------------------------------- +# Enable/Disable DAuth communication at all +# In case isn't defined default value is false +# allow_dauth_login=false +# Define comma separated list of allowed IP addresses +# dauth.host=127.0.0.1 +# Define secret used to validate JWT token +# jwt.token_secret=secret +# -------------------------------------- DAuth login -- + # Disable akka (Remote storage not possible) use_akka=false diff --git a/obp-api/src/main/scala/code/api/DAuthLogin.scala b/obp-api/src/main/scala/code/api/DAuthLogin.scala new file mode 100755 index 000000000..adf259bc9 --- /dev/null +++ b/obp-api/src/main/scala/code/api/DAuthLogin.scala @@ -0,0 +1,333 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2019, TESOBE GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + + This product includes software developed at + TESOBE (http://www.tesobe.com/) + + */ +package code.api + +import code.api.JSONFactoryDAuth.PayloadOfJwtJSON +import code.api.util._ +import code.consumer.Consumers +import code.model.{Consumer, UserX} +import code.users.Users +import code.util.Helper.MdcLoggable +import com.nimbusds.jwt.JWTClaimsSet +import com.openbankproject.commons.model.{User} +import net.liftweb.common._ +import net.liftweb.http._ +import net.liftweb.http.rest.RestHelper +import net.liftweb.json._ +import net.liftweb.util.Helpers + +import com.openbankproject.commons.ExecutionContext.Implicits.global +import scala.concurrent.Future + +/** + * This object provides the API calls necessary to + * authenticate users using JSON Web Tokens (http://jwt.io). + */ + + +object JSONFactoryDAuth { + //Never update these values inside the case class + case class PayloadOfJwtJSON( + smart_contract_address: String, + network_name: String, + msg_sender: String, + consumer_id: String, + time_stamp: String, + caller_request_id: String + ) + +} + +object DAuthLogin extends RestHelper with MdcLoggable { + + val DAuth = "DAuth" // This value is used for ResourceUser.provider and Consumer.description + + def createJwt(payloadAsJsonString: String) : String = { + val smartContractAddress = getFieldFromPayloadJson(payloadAsJsonString, "smart_contract_address") + val networkName = getFieldFromPayloadJson(payloadAsJsonString, "network_name") + val msgSender = getFieldFromPayloadJson(payloadAsJsonString, "msg_sender") + val consumerId = getFieldFromPayloadJson(payloadAsJsonString, "consumer_id") + val timeStamp = getFieldFromPayloadJson(payloadAsJsonString, "time_stamp") + val callerRequestId = getFieldFromPayloadJson(payloadAsJsonString, "caller_request_id") + + val json = JSONFactoryDAuth.PayloadOfJwtJSON( + smart_contract_address = smartContractAddress, + network_name = networkName, + msg_sender = msgSender, + consumer_id = consumerId, + time_stamp = timeStamp, + caller_request_id = callerRequestId + ) + val jwtPayloadAsJson = compactRender(Extraction.decompose(json)) + val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) + + APIUtil.getPropsAsBoolValue("jwt.use.ssl", false) match { + case true => + CertificateUtil.encryptJwtWithRsa(jwtClaims) + case false => + CertificateUtil.jwtWithHmacProtection(jwtClaims) + } + } + + def parseJwt(parameters: Map[String, String]): Box[String] = { + val jwt = getToken(parameters) + logger.debug("parseJwt says jwt.toString is: " + jwt.toString) + logger.debug("parseJwt says: validateJwtToken(jwt) is:" + validateJwtToken(jwt)) + validateJwtToken(jwt) match { + case Full(jwtPayload) => + logger.debug("parseJwt says: Full: " + jwtPayload.toString) + Full(compactRender(Extraction.decompose(jwtPayload))) + case _ => + logger.debug("parseJwt says: Not Full(jwtPayload)") + Failure(ErrorMessages.GatewayLoginJwtTokenIsNotValid) + } + } + + def validateJwtToken(token: String): Box[PayloadOfJwtJSON] = { + APIUtil.getPropsAsBoolValue("jwt.use.ssl", false) match { + case true => + logger.debug("validateJwtToken says: verifying jwt token with RSA: " + token) + val claim = CertificateUtil.decryptJwtWithRsa(token) + Box(parse(claim.toString).extractOpt[PayloadOfJwtJSON]) + case false => + logger.debug("validateJwtToken says: verifying jwt token with HmacProtection: " + token) + logger.debug(CertificateUtil.verifywtWithHmacProtection(token).toString) + CertificateUtil.verifywtWithHmacProtection(token) match { + case true => + logger.debug("validateJwtToken says: jwt is verified: " + token) + val claim = CertificateUtil.parseJwtWithHmacProtection(token) + logger.debug("validateJwtToken says: this is claim of verified jwt: " + claim.toString()) + Box(parse(claim.toString).extractOpt[PayloadOfJwtJSON]) + case _ => + logger.debug("validateJwtToken says: could not verify jwt") + Failure(ErrorMessages.GatewayLoginJwtTokenIsNotValid) + } + } + } + + // Check if the request (access token or request token) is valid and return a tuple + def validator(request: Box[Req]) : (Int, String, Map[String,String]) = { + // First we try to extract all parameters from a Request + val parameters: Map[String, String] = getAllParameters(request) + val emptyMap = Map[String, String]() + + parameters.get("error") match { + case Some(m) => { + logger.error("DAuthLogin error message : " + m) + (400, m, emptyMap) + } + case _ => { + // Are all the necessary DAuthLogin parameters present? + val missingParams: Set[String] = missingDAuthLoginParameters(parameters) + missingParams.nonEmpty match { + case true => { + val message = ErrorMessages.DAuthLoginMissingParameters + missingParams.mkString(", ") + logger.error("DAuthLogin error message : " + message) + (400, message, emptyMap) + } + case false => { + logger.debug("DAuthLogin parameters : " + parameters) + (200, "", parameters) + } + } + } + } + } + + def getOrCreateResourceUser(jwtPayload: String, callContext: Option[CallContext]) : Box[(User, Option[CallContext])] = { + val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + logger.debug("login_user_name: " + username) + for { + tuple <- + Users.users.vend.getUserByProviderId(provider = DAuth, idGivenByProvider = username).or { // Find a user + Users.users.vend.createResourceUser( // Otherwise create a new one + provider = DAuth, + providerId = Some(username), + None, + name = Some(username), + email = None, + userId = None, + createdByUserInvitationId = None, + company = None, + lastMarketingAgreementSignedDate = None + ) + } match { + case Full(u) => + Full((u,callContext)) // Return user + case Empty => + Failure(ErrorMessages.DAuthLoginCannotGetOrCreateUser) + case Failure(msg, t, c) => + Failure(msg, t, c) + case _ => + Failure(ErrorMessages.DAuthLoginUnknownError) + } + } yield { + tuple + } + } + def getOrCreateResourceUserFuture(jwtPayload: String, callContext: Option[CallContext]) : Future[Box[(User, Option[CallContext])]] = { + val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + logger.debug("login_user_name: " + username) + for { + tuple <- + Users.users.vend.getOrCreateUserByProviderIdFuture(provider = DAuth, idGivenByProvider = username, consentId = None, name = None, email = None) map { + case (Full(u), _) => + Full(u, callContext) // Return user + case (Empty, _) => + Failure(ErrorMessages.DAuthLoginCannotGetOrCreateUser) + case (Failure(msg, t, c), _) => + Failure(msg, t, c) + case _ => + Failure(ErrorMessages.DAuthLoginUnknownError) + } + } yield { + tuple + } + } + + def getOrCreateConsumer(jwtPayload: String, u: User) : Box[Consumer] = { + val consumerId = getFieldFromPayloadJson(jwtPayload, "consumer_id") + val consumerName = getFieldFromPayloadJson(jwtPayload, "msg_sender") + logger.debug("app_id: " + consumerId) + logger.debug("app_name: " + consumerName) + Consumers.consumers.vend.getOrCreateConsumer( + consumerId=Some(consumerId), + Some(Helpers.randomString(40).toLowerCase), + Some(Helpers.randomString(40).toLowerCase), + None, + None, + None, + None, + Some(true), + name = Some(consumerName), + appType = None, + description = Some(DAuth), + developerEmail = None, + redirectURL = None, + createdByUserId = Some(u.userId) + ) + } + + // Return a Map containing the DAuthLogin parameter : token -> value + def getAllParameters(request: Box[Req]): Map[String, String] = { + def toMap(parametersList: String) = { + //transform the string "DAuthLogin token="value"" + //to a tuple (DAuthLogin_parameter,Decoded(value)) + def dynamicListExtract(input: String) = { + val DAuthLoginPossibleParameters = + List( + "token" + ) + if (input contains "=") { + val split = input.split("=", 2) + val parameterValue = split(1).replace("\"", "") + //add only OAuth parameters and not empty + if (DAuthLoginPossibleParameters.contains(split(0)) && !parameterValue.isEmpty) + Some(split(0), parameterValue) // return key , value + else + None + } + else + None + } + // We delete the "DAuthLogin" prefix and all the white spaces that may exist in the string + val cleanedParameterList = parametersList.stripPrefix("DAuthLogin").replaceAll("\\s", "") + val params = Map(cleanedParameterList.split(",").flatMap(dynamicListExtract _): _*) + params + } + + request match { + case Full(a) => a.header("Authorization") match { + case Full(header) => { + if (header.contains("DAuthLogin")) + toMap(header) + else + Map("error" -> "Missing DAuthLogin in header!") + } + case _ => Map("error" -> "Missing Authorization header!") + } + case _ => Map("error" -> "Request is incorrect!") + } + } + + // Returns the missing parameters + def missingDAuthLoginParameters(parameters: Map[String, String]): Set[String] = { + ("token" :: List()).toSet diff parameters.keySet + } + + private def getToken(params: Map[String, String]): String = { + logger.debug("getToken params are: " + params.toString()) + val token = params.getOrElse("token", "") + logger.debug("getToken wants to return token: " + token) + token + } + + private def getFieldFromPayloadJson(payloadAsJsonString: String, fieldName: String) = { + val jwtJson = parse(payloadAsJsonString) // Transform Json string to JsonAST + val v = jwtJson.\(fieldName) + v match { + case JNothing => + "" + case _ => + compactRender(v).replace("\"", "") + } + } + + // Try to find errorCode in Json string received from South side and extract to list + // Return list of error codes values + def getErrors(message: String) : List[String] = { + val json = parse(message) removeField { + case JField("backendMessages", _) => true + case _ => false + } + val listOfValues = for { + JArray(objects) <- json + JObject(obj) <- objects + JField("errorCode", JString(fieldName)) <- obj + } yield fieldName + listOfValues + } + + + def getUser : Box[User] = { + val (httpCode, message, parameters) = DAuthLogin.validator(S.request) + httpCode match { + case 200 => + val payload = DAuthLogin.parseJwt(parameters) + payload match { + case Full(payload) => + val username = getFieldFromPayloadJson(payload, "smart_contract_address") + logger.debug("username: " + username) + Users.users.vend.getUserByProviderId(provider = DAuth, idGivenByProvider = username) + case _ => + None + } + case _ => + None + } + } +} diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 2bcf154ab..1def75b97 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -401,7 +401,48 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { case _ => Failure(ErrorMessages.GatewayLoginUnknownError) } - } else { + } + else if (APIUtil.getPropsAsBoolValue("allow_dauth_login", false) && hasDAuthHeader(authorization)) { + logger.info("allow_dauth_login-getRemoteIpAddress: " + remoteIpAddress ) + APIUtil.getPropsValue("dauth.host") match { + case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature + val s = S + val (httpCode, message, parameters) = DAuthLogin.validator(s.request) + httpCode match { + case 200 => + val payload = DAuthLogin.parseJwt(parameters) + payload match { + case Full(payload) => + val s = S + DAuthLogin.getOrCreateResourceUser(payload: String, Some(cc)) match { + case Full((u, callContext)) => // Authentication is successful + val consumer = DAuthLogin.getOrCreateConsumer(payload, u) + setGatewayResponseHeader(s) {DAuthLogin.createJwt(payload)} + val jwt = DAuthLogin.createJwt(payload) + val callContextUpdated = ApiSession.updateCallContext(DAuthLoginResponseHeader(Some(jwt)), callContext) + fn(callContextUpdated.map( callContext =>callContext.copy(user = Full(u), consumer = consumer)).getOrElse(callContext.getOrElse(cc).copy(user = Full(u), consumer = consumer))) + case Failure(msg, t, c) => Failure(msg, t, c) + case _ => Full(errorJsonResponse(payload, httpCode)) + } + case Failure(msg, t, c) => + Failure(msg, t, c) + case _ => + Failure(ErrorMessages.DAuthLoginUnknownError) + } + case _ => + Failure(message) + } + case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == false) => // All other addresses will be rejected + Failure(ErrorMessages.DAuthLoginWhiteListAddresses) + case Empty => + Failure(ErrorMessages.DAuthLoginHostPropertyMissing) // There is no dauth.host in props file + case Failure(msg, t, c) => + Failure(msg, t, c) + case _ => + Failure(ErrorMessages.DAuthLoginUnknownError) + } + } + else { fn(cc) } } diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 858072ba9..511af54ee 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -180,6 +180,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ def hasAnOAuth2Header(authorization: Box[String]): Boolean = hasHeader("Bearer", authorization) def hasGatewayHeader(authorization: Box[String]) = hasHeader("GatewayLogin", authorization) + + def hasDAuthHeader(authorization: Box[String]) = hasHeader("DAuthLogin", authorization) /** * Helper function which tells us does an "Authorization" request header field has the Type of an authentication scheme @@ -2244,6 +2246,31 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ None } } + /** + * Defines DAuth Custom Response Header. + */ + val DAuthResponseHeaderName = "DAuthLogin" + /** + * Set value of DAuth Custom Response Header. + */ + def setDAuthResponseHeader(s: S)(value: String) = s.setSessionAttribute(DAuthResponseHeaderName, value) + /** + * @return - DAuth Custom Response Header. + */ + def getDAuthResponseHeader() = { + S.getSessionAttribute(DAuthResponseHeaderName) match { + case Full(h) => List((DAuthResponseHeaderName, h)) + case _ => Nil + } + } + def getDAuthLoginJwt(): Option[String] = { + getDAuthResponseHeader() match { + case x :: Nil => + Some(x._2) + case _ => + None + } + } /** * Turn a string of format "FooBar" into snake case "foo_bar" @@ -2708,7 +2735,47 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case _ => Future { (Failure(ErrorMessages.GatewayLoginUnknownError), None) } } - } else if(Option(cc).flatMap(_.user).isDefined) { + } // DAuthLogin Login + else if (getPropsAsBoolValue("allow_dauth_login", false) && hasDAuthHeader(cc.authReqHeaderField)) { + logger.info("allow_dauth_login-getRemoteIpAddress: " + remoteIpAddress ) + APIUtil.getPropsValue("dauth.host") match { + case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature + val (httpCode, message, parameters) = DAuthLogin.validator(s.request) + httpCode match { + case 200 => + val payload = DAuthLogin.parseJwt(parameters) + payload match { + case Full(payload) => + DAuthLogin.getOrCreateResourceUserFuture(payload: String, Some(cc)) map { + case Full((u,callContext)) => // Authentication is successful + val consumer = DAuthLogin.getOrCreateConsumer(payload, u) + val jwt = DAuthLogin.createJwt(payload) + val callContextUpdated = ApiSession.updateCallContext(DAuthLoginResponseHeader(Some(jwt)), callContext) + (Full(u), callContextUpdated.map(_.copy(consumer=consumer, user = Full(u)))) + case Failure(msg, t, c) => + (Failure(msg, t, c), None) + case _ => + (Failure(payload), None) + } + case Failure(msg, t, c) => + Future { (Failure(msg, t, c), None) } + case _ => + Future { (Failure(ErrorMessages.DAuthLoginUnknownError), None) } + } + case _ => + Future { (Failure(message), None) } + } + case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == false) => // All other addresses will be rejected + Future { (Failure(ErrorMessages.DAuthLoginWhiteListAddresses), None) } + case Empty => + Future { (Failure(ErrorMessages.DAuthLoginHostPropertyMissing), None) } // There is no dauth.host in props file + case Failure(msg, t, c) => + Future { (Failure(msg, t, c), None) } + case _ => + Future { (Failure(ErrorMessages.DAuthLoginUnknownError), None) } + } + } + else if(Option(cc).flatMap(_.user).isDefined) { Future{(cc.user, Some(cc))} } else { diff --git a/obp-api/src/main/scala/code/api/util/ApiSession.scala b/obp-api/src/main/scala/code/api/util/ApiSession.scala index eb84aa7ee..af9e7c08b 100644 --- a/obp-api/src/main/scala/code/api/util/ApiSession.scala +++ b/obp-api/src/main/scala/code/api/util/ApiSession.scala @@ -1,10 +1,11 @@ package code.api.util +import code.api.JSONFactoryDAuth import java.util.{Date, UUID} import code.api.JSONFactoryGateway.PayloadOfJwtJSON import code.api.oauth1a.OauthParams._ import code.api.util.APIUtil._ -import code.api.util.AuthenticationType.{Anonymous, DirectLogin, GatewayLogin, OAuth2_OIDC, OAuth2_OIDC_FAPI} +import code.api.util.AuthenticationType.{Anonymous, DirectLogin, GatewayLogin, DAuthLogin, OAuth2_OIDC, OAuth2_OIDC_FAPI} import code.api.util.ErrorMessages.{BankAccountNotFound, UserNotLoggedIn} import code.api.util.RateLimitingJson.CallLimit import code.context.UserAuthContextProvider @@ -25,6 +26,8 @@ import scala.collection.immutable.List case class CallContext( gatewayLoginRequestPayload: Option[PayloadOfJwtJSON] = None, //Never update these values inside the case class !!! gatewayLoginResponseHeader: Option[String] = None, + dauthLoginRequestPayload: Option[JSONFactoryDAuth.PayloadOfJwtJSON] = None, //Never update these values inside the case class !!! + dauthLoginResponseHeader: Option[String] = None, spelling: Option[String] = None, user: Box[User] = Empty, consumer: Box[Consumer] = Empty, @@ -137,6 +140,8 @@ case class CallContext( def authType: AuthenticationType = { if(hasGatewayHeader(authReqHeaderField)) { GatewayLogin + } else if(hasDAuthHeader(authReqHeaderField)) { // DAuth Login + DAuthLogin } else if(has2021DirectLoginHeader(requestHeaders)) { // Direct Login DirectLogin } else if(hasDirectLoginHeader(authReqHeaderField)) { // Direct Login Deprecated @@ -161,6 +166,7 @@ object AuthenticationType extends OBPEnumeration[AuthenticationType]{ override def toString: String = "OAuth1.0a" } object GatewayLogin extends AuthenticationType + object DAuthLogin extends AuthenticationType object OAuth2_OIDC extends AuthenticationType object OAuth2_OIDC_FAPI extends AuthenticationType object Anonymous extends AuthenticationType @@ -193,9 +199,11 @@ case class CallContextLight(gatewayLoginRequestPayload: Option[PayloadOfJwtJSON] `X-Rate-Limit-Reset` : Long = -1 ) -trait GatewayLoginParam -case class GatewayLoginRequestPayload(jwtPayload: Option[PayloadOfJwtJSON]) extends GatewayLoginParam -case class GatewayLoginResponseHeader(jwt: Option[String]) extends GatewayLoginParam +trait LoginParam +case class GatewayLoginRequestPayload(jwtPayload: Option[PayloadOfJwtJSON]) extends LoginParam +case class GatewayLoginResponseHeader(jwt: Option[String]) extends LoginParam +case class DAuthLoginRequestPayload(jwtPayload: Option[JSONFactoryDAuth.PayloadOfJwtJSON]) extends LoginParam +case class DAuthLoginResponseHeader(jwt: Option[String]) extends LoginParam case class Spelling(spelling: Box[String]) @@ -230,17 +238,17 @@ object ApiSession { def updateCallContext(s: Spelling, cnt: Option[CallContext]): Option[CallContext] = { cnt match { case None => - Some(CallContext(gatewayLoginRequestPayload = None, gatewayLoginResponseHeader = None, spelling = s.spelling)) + Some(CallContext(spelling = s.spelling)) //Some fields default value is NONE. case Some(v) => Some(v.copy(spelling = s.spelling)) } } - def updateCallContext(jwt: GatewayLoginParam, cnt: Option[CallContext]): Option[CallContext] = { + def updateCallContext(jwt: LoginParam, cnt: Option[CallContext]): Option[CallContext] = { jwt match { - case GatewayLoginRequestPayload(None) => + case GatewayLoginRequestPayload(None) | DAuthLoginRequestPayload(None) => cnt - case GatewayLoginResponseHeader(None) => + case GatewayLoginResponseHeader(None) | DAuthLoginResponseHeader(None) => cnt case GatewayLoginRequestPayload(Some(jwtPayload)) => cnt match { @@ -256,6 +264,20 @@ object ApiSession { case None => Some(CallContext(gatewayLoginRequestPayload = None, gatewayLoginResponseHeader = Some(j), spelling = None)) } + case DAuthLoginRequestPayload(Some(jwtPayload)) => + cnt match { + case Some(v) => + Some(v.copy(dauthLoginRequestPayload = Some(jwtPayload))) + case None => + Some(CallContext(dauthLoginRequestPayload = Some(jwtPayload), dauthLoginResponseHeader = None, spelling = None)) + } + case DAuthLoginResponseHeader(Some(j)) => + cnt match { + case Some(v) => + Some(v.copy(dauthLoginResponseHeader = Some(j))) + case None => + Some(CallContext(dauthLoginRequestPayload = None, dauthLoginResponseHeader = Some(j), spelling = None)) + } } } diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index f346ec152..1193a6936 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -182,6 +182,17 @@ object ErrorMessages { val UserIsDeleted = "OBP-20064: The user is deleted!" + val DAuthLoginCannotGetOrCreateUser = "OBP-20065: Cannot get or create user during DAuthLogin process." + val DAuthLoginMissingParameters = "OBP-20066: These DAuthLogin parameters are missing: " + val DAuthLoginUnknownError = "OBP-20067: Unknown Gateway login error." + val DAuthLoginHostPropertyMissing = "OBP-20068: Property gateway.host is not defined." + val DAuthLoginWhiteListAddresses = "OBP-20069: Gateway login can be done only from allowed addresses." + val DAuthLoginJwtTokenIsNotValid = "OBP-20070: The JWT is corrupted/changed during a transport." + val DAuthLoginCannotExtractJwtToken = "OBP-20071: Header, Payload and Signature cannot be extracted from the JWT." + val DAuthLoginNoNeedToCallCbs = "OBP-20072: There is no need to call CBS" + val DAuthLoginCannotFindUser = "OBP-20073: User cannot be found. Please initiate CBS communication in order to create it." + val DAuthLoginCannotGetCbsToken = "OBP-20074: Cannot get the CBSToken response from South side" + val DAuthLoginNoJwtForResponse = "OBP-20075: There is no useful value for JWT." val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " From f84701db9eb0ab6aae23f3087a9cd05b1b119457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 1 Nov 2021 17:15:25 +0100 Subject: [PATCH 096/185] docfix/Add jwt.token_secret=secret as the name for GatawayLoin and DAuthLogin --- obp-api/src/main/resources/props/sample.props.template | 2 +- obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala | 4 ++++ obp-api/src/main/scala/code/api/util/CertificateUtil.scala | 2 +- obp-api/src/main/scala/code/api/util/Glossary.scala | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index d8898b4bc..dd1ffe878 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -685,7 +685,7 @@ autocomplete_at_login_form_enabled=false # Define comma separated list of allowed IP addresses # gateway.host=127.0.0.1 # Define secret used to validate JWT token -# gateway.token_secret=secret +# jwt.token_secret=secret # -------------------------------------- Gateway login -- diff --git a/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala b/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala index 1492be369..f9f96e35f 100644 --- a/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala +++ b/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala @@ -29,6 +29,10 @@ object ApiPropsWithAlias { name="allow_customer_firehose", alias="allow_firehose_views", defaultValue="false") + def jwtTokenSecret = getValueByNameOrAliasAsBoolean( + name="jwt.token_secret", + alias="gateway.token_secret", + defaultValue="Cannot get your at least 256 bit secret") } object HelperFunctions extends MdcLoggable { diff --git a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala index d6cd3bdca..4b3774ac7 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala @@ -25,7 +25,7 @@ object CryptoSystem extends Enumeration { object CertificateUtil extends MdcLoggable { // your-at-least-256-bit-secret - val sharedSecret = APIUtil.getPropsValue("gateway.token_secret", "Cannot get your at least 256 bit secret") + val sharedSecret = ApiPropsWithAlias.jwtTokenSecret lazy val (publicKey: RSAPublicKey, privateKey: RSAPrivateKey) = APIUtil.getPropsAsBoolValue("jwt.use.ssl", false) match { case true => 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 7a6ea4f12..4f4a54e21 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -1844,10 +1844,10 @@ object Glossary extends MdcLoggable { |# Define comma separated list of allowed IP addresses |# gateway.host=127.0.0.1 |# Define secret used to validate JWT token -|# gateway.token_secret=secret +|# jwt.token_secret=secret |# -------------------------------------- Gateway login -- |``` -|Please keep in mind that property gateway.token_secret is used to validate JWT token to check it is not changed or corrupted during transport. +|Please keep in mind that property jwt.token_secret is used to validate JWT token to check it is not changed or corrupted during transport. | |### 2) Create / have access to a JWT | From ca40434443ce97309ce3780cf01f941782cebbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 2 Nov 2021 08:41:39 +0100 Subject: [PATCH 097/185] docfix/Add jwt.token_secret=secret as the name for GatawayLoin and DAuthLogin 2 --- obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala | 2 +- obp-api/src/main/scala/code/api/util/CertificateUtil.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala b/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala index f9f96e35f..2f662a5d5 100644 --- a/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala +++ b/obp-api/src/main/scala/code/api/util/ApiPropsWithAlias.scala @@ -29,7 +29,7 @@ object ApiPropsWithAlias { name="allow_customer_firehose", alias="allow_firehose_views", defaultValue="false") - def jwtTokenSecret = getValueByNameOrAliasAsBoolean( + def jwtTokenSecret = getValueByNameOrAlias( name="jwt.token_secret", alias="gateway.token_secret", defaultValue="Cannot get your at least 256 bit secret") diff --git a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala index 4b3774ac7..1baea2fee 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala @@ -25,7 +25,7 @@ object CryptoSystem extends Enumeration { object CertificateUtil extends MdcLoggable { // your-at-least-256-bit-secret - val sharedSecret = ApiPropsWithAlias.jwtTokenSecret + val sharedSecret: String = ApiPropsWithAlias.jwtTokenSecret lazy val (publicKey: RSAPublicKey, privateKey: RSAPrivateKey) = APIUtil.getPropsAsBoolValue("jwt.use.ssl", false) match { case true => From 691d37c77817ed6a5113a63399be57739d1c2a8a Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 2 Nov 2021 11:48:10 +0100 Subject: [PATCH 098/185] feature/tweaked the error message for InvalidChallengeAnswer --- obp-api/src/main/scala/code/api/util/ErrorMessages.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 1193a6936..a8b878d63 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -503,7 +503,9 @@ object ErrorMessages { val InvalidChargePolicy = "OBP-40013: Invalid Charge Policy. Please specify a valid value for Charge_Policy: SHARED, SENDER or RECEIVER. " val AllowedAttemptsUsedUp = "OBP-40014: Sorry, you've used up your allowed attempts. " val InvalidChallengeType = "OBP-40015: Invalid Challenge Type. Please specify a valid value for CHALLENGE_TYPE, when you create the transaction request." - val InvalidChallengeAnswer = "OBP-40016: Invalid Challenge Answer. Please specify a valid value for answer in Json body. If it is sandbox mode, the answer must be `123`. If it kafka mode, the answer can be got by phone message or other security ways." + val InvalidChallengeAnswer = "OBP-40016: Invalid Challenge Answer. Please specify a valid value for answer in Json body. " + + "If connector = mapped and transactionRequestType_OTP_INSTRUCTION_TRANSPORT = DUMMY and suggested_default_sca_method=DUMMY, the answer must be `123`. " + + "If connector = others, the challenge answer can be got by phone message or other security ways." val InvalidPhoneNumber = "OBP-40017: Invalid Phone Number. Please specify a valid value for PHONE_NUMBER. Eg:+9722398746 " val TransactionRequestsNotEnabled = "OBP-40018: Sorry, Transaction Requests are not enabled in this API instance." val NextChallengePending = s"OBP-40019: Cannot create transaction due to transaction request is in status: ${NEXT_CHALLENGE_PENDING}." From 7950d278293caf30f50530fb892c18daff86950a Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 2 Nov 2021 13:41:07 +0100 Subject: [PATCH 099/185] feature/rename DAuthLogin --> DAuth --- .../resources/props/sample.props.template | 11 ++-- .../main/scala/code/api/OBPRestHelper.scala | 22 +++---- .../api/{DAuthLogin.scala => dauth.scala} | 58 +++++++++---------- .../main/scala/code/api/util/APIUtil.scala | 28 ++++----- .../main/scala/code/api/util/ApiSession.scala | 30 +++++----- .../scala/code/api/util/ErrorMessages.scala | 17 ++---- 6 files changed, 81 insertions(+), 85 deletions(-) rename obp-api/src/main/scala/code/api/{DAuthLogin.scala => dauth.scala} (84%) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 1b03dbb96..54ef0d035 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -678,15 +678,17 @@ autocomplete_at_login_form_enabled=false # control access to customer firehose. # allow_customer_firehose=false +# -- Gateway And DAuth common Setting-------------------------------------- +# Define secret used to validate JWT token +# jwt.token_secret=secretsecretsecretstsecretssssss + # -- Gateway login -------------------------------------- # Enable/Disable Gateway communication at all # In case isn't defined default value is false # allow_gateway_login=false # Define comma separated list of allowed IP addresses # gateway.host=127.0.0.1 -# Define secret used to validate JWT token -# jwt.token_secret=secret -# -------------------------------------- Gateway login -- + # -- DAuth login -------------------------------------- # Enable/Disable DAuth communication at all @@ -694,8 +696,7 @@ autocomplete_at_login_form_enabled=false # allow_dauth_login=false # Define comma separated list of allowed IP addresses # dauth.host=127.0.0.1 -# Define secret used to validate JWT token -# jwt.token_secret=secret + # -------------------------------------- DAuth login -- diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 1def75b97..6475ae593 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -407,19 +407,19 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature val s = S - val (httpCode, message, parameters) = DAuthLogin.validator(s.request) + val (httpCode, message, parameters) = DAuth.validator(s.request) httpCode match { case 200 => - val payload = DAuthLogin.parseJwt(parameters) + val payload = DAuth.parseJwt(parameters) payload match { case Full(payload) => val s = S - DAuthLogin.getOrCreateResourceUser(payload: String, Some(cc)) match { + DAuth.getOrCreateResourceUser(payload: String, Some(cc)) match { case Full((u, callContext)) => // Authentication is successful - val consumer = DAuthLogin.getOrCreateConsumer(payload, u) - setGatewayResponseHeader(s) {DAuthLogin.createJwt(payload)} - val jwt = DAuthLogin.createJwt(payload) - val callContextUpdated = ApiSession.updateCallContext(DAuthLoginResponseHeader(Some(jwt)), callContext) + val consumer = DAuth.getOrCreateConsumer(payload, u) + setGatewayResponseHeader(s) {DAuth.createJwt(payload)} + val jwt = DAuth.createJwt(payload) + val callContextUpdated = ApiSession.updateCallContext(DAuthResponseHeader(Some(jwt)), callContext) fn(callContextUpdated.map( callContext =>callContext.copy(user = Full(u), consumer = consumer)).getOrElse(callContext.getOrElse(cc).copy(user = Full(u), consumer = consumer))) case Failure(msg, t, c) => Failure(msg, t, c) case _ => Full(errorJsonResponse(payload, httpCode)) @@ -427,19 +427,19 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { case Failure(msg, t, c) => Failure(msg, t, c) case _ => - Failure(ErrorMessages.DAuthLoginUnknownError) + Failure(ErrorMessages.DAuthUnknownError) } case _ => Failure(message) } case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == false) => // All other addresses will be rejected - Failure(ErrorMessages.DAuthLoginWhiteListAddresses) + Failure(ErrorMessages.DAuthWhiteListAddresses) case Empty => - Failure(ErrorMessages.DAuthLoginHostPropertyMissing) // There is no dauth.host in props file + Failure(ErrorMessages.DAuthHostPropertyMissing) // There is no dauth.host in props file case Failure(msg, t, c) => Failure(msg, t, c) case _ => - Failure(ErrorMessages.DAuthLoginUnknownError) + Failure(ErrorMessages.DAuthUnknownError) } } else { diff --git a/obp-api/src/main/scala/code/api/DAuthLogin.scala b/obp-api/src/main/scala/code/api/dauth.scala similarity index 84% rename from obp-api/src/main/scala/code/api/DAuthLogin.scala rename to obp-api/src/main/scala/code/api/dauth.scala index adf259bc9..7c0cc6898 100755 --- a/obp-api/src/main/scala/code/api/DAuthLogin.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -62,9 +62,9 @@ object JSONFactoryDAuth { } -object DAuthLogin extends RestHelper with MdcLoggable { +object DAuth extends RestHelper with MdcLoggable { - val DAuth = "DAuth" // This value is used for ResourceUser.provider and Consumer.description + val DAuthValue = "DAuth" // This value is used for ResourceUser.provider and Consumer.description def createJwt(payloadAsJsonString: String) : String = { val smartContractAddress = getFieldFromPayloadJson(payloadAsJsonString, "smart_contract_address") @@ -137,20 +137,20 @@ object DAuthLogin extends RestHelper with MdcLoggable { parameters.get("error") match { case Some(m) => { - logger.error("DAuthLogin error message : " + m) + logger.error("DAuth error message : " + m) (400, m, emptyMap) } case _ => { - // Are all the necessary DAuthLogin parameters present? - val missingParams: Set[String] = missingDAuthLoginParameters(parameters) + // Are all the necessary DAuth parameters present? + val missingParams: Set[String] = missingDAuthParameters(parameters) missingParams.nonEmpty match { case true => { - val message = ErrorMessages.DAuthLoginMissingParameters + missingParams.mkString(", ") - logger.error("DAuthLogin error message : " + message) + val message = ErrorMessages.DAuthMissingParameters + missingParams.mkString(", ") + logger.error("DAuth error message : " + message) (400, message, emptyMap) } case false => { - logger.debug("DAuthLogin parameters : " + parameters) + logger.debug("DAuth parameters : " + parameters) (200, "", parameters) } } @@ -163,9 +163,9 @@ object DAuthLogin extends RestHelper with MdcLoggable { logger.debug("login_user_name: " + username) for { tuple <- - Users.users.vend.getUserByProviderId(provider = DAuth, idGivenByProvider = username).or { // Find a user + Users.users.vend.getUserByProviderId(provider = DAuthValue, idGivenByProvider = username).or { // Find a user Users.users.vend.createResourceUser( // Otherwise create a new one - provider = DAuth, + provider = DAuthValue, providerId = Some(username), None, name = Some(username), @@ -179,11 +179,11 @@ object DAuthLogin extends RestHelper with MdcLoggable { case Full(u) => Full((u,callContext)) // Return user case Empty => - Failure(ErrorMessages.DAuthLoginCannotGetOrCreateUser) + Failure(ErrorMessages.DAuthCannotGetOrCreateUser) case Failure(msg, t, c) => Failure(msg, t, c) case _ => - Failure(ErrorMessages.DAuthLoginUnknownError) + Failure(ErrorMessages.DAuthUnknownError) } } yield { tuple @@ -194,15 +194,15 @@ object DAuthLogin extends RestHelper with MdcLoggable { logger.debug("login_user_name: " + username) for { tuple <- - Users.users.vend.getOrCreateUserByProviderIdFuture(provider = DAuth, idGivenByProvider = username, consentId = None, name = None, email = None) map { + Users.users.vend.getOrCreateUserByProviderIdFuture(provider = DAuthValue, idGivenByProvider = username, consentId = None, name = None, email = None) map { case (Full(u), _) => Full(u, callContext) // Return user case (Empty, _) => - Failure(ErrorMessages.DAuthLoginCannotGetOrCreateUser) + Failure(ErrorMessages.DAuthCannotGetOrCreateUser) case (Failure(msg, t, c), _) => Failure(msg, t, c) case _ => - Failure(ErrorMessages.DAuthLoginUnknownError) + Failure(ErrorMessages.DAuthUnknownError) } } yield { tuple @@ -225,20 +225,20 @@ object DAuthLogin extends RestHelper with MdcLoggable { Some(true), name = Some(consumerName), appType = None, - description = Some(DAuth), + description = Some(DAuthValue), developerEmail = None, redirectURL = None, createdByUserId = Some(u.userId) ) } - // Return a Map containing the DAuthLogin parameter : token -> value + // Return a Map containing the DAuth parameter : token -> value def getAllParameters(request: Box[Req]): Map[String, String] = { def toMap(parametersList: String) = { - //transform the string "DAuthLogin token="value"" - //to a tuple (DAuthLogin_parameter,Decoded(value)) + //transform the string "DAuth token="value"" + //to a tuple (DAuth_parameter,Decoded(value)) def dynamicListExtract(input: String) = { - val DAuthLoginPossibleParameters = + val DAuthPossibleParameters = List( "token" ) @@ -246,7 +246,7 @@ object DAuthLogin extends RestHelper with MdcLoggable { val split = input.split("=", 2) val parameterValue = split(1).replace("\"", "") //add only OAuth parameters and not empty - if (DAuthLoginPossibleParameters.contains(split(0)) && !parameterValue.isEmpty) + if (DAuthPossibleParameters.contains(split(0)) && !parameterValue.isEmpty) Some(split(0), parameterValue) // return key , value else None @@ -254,8 +254,8 @@ object DAuthLogin extends RestHelper with MdcLoggable { else None } - // We delete the "DAuthLogin" prefix and all the white spaces that may exist in the string - val cleanedParameterList = parametersList.stripPrefix("DAuthLogin").replaceAll("\\s", "") + // We delete the "DAuth" prefix and all the white spaces that may exist in the string + val cleanedParameterList = parametersList.stripPrefix("DAuth").replaceAll("\\s", "") val params = Map(cleanedParameterList.split(",").flatMap(dynamicListExtract _): _*) params } @@ -263,10 +263,10 @@ object DAuthLogin extends RestHelper with MdcLoggable { request match { case Full(a) => a.header("Authorization") match { case Full(header) => { - if (header.contains("DAuthLogin")) + if (header.contains("DAuth")) toMap(header) else - Map("error" -> "Missing DAuthLogin in header!") + Map("error" -> "Missing DAuth in header!") } case _ => Map("error" -> "Missing Authorization header!") } @@ -275,7 +275,7 @@ object DAuthLogin extends RestHelper with MdcLoggable { } // Returns the missing parameters - def missingDAuthLoginParameters(parameters: Map[String, String]): Set[String] = { + def missingDAuthParameters(parameters: Map[String, String]): Set[String] = { ("token" :: List()).toSet diff parameters.keySet } @@ -314,15 +314,15 @@ object DAuthLogin extends RestHelper with MdcLoggable { def getUser : Box[User] = { - val (httpCode, message, parameters) = DAuthLogin.validator(S.request) + val (httpCode, message, parameters) = DAuth.validator(S.request) httpCode match { case 200 => - val payload = DAuthLogin.parseJwt(parameters) + val payload = DAuth.parseJwt(parameters) payload match { case Full(payload) => val username = getFieldFromPayloadJson(payload, "smart_contract_address") logger.debug("username: " + username) - Users.users.vend.getUserByProviderId(provider = DAuth, idGivenByProvider = username) + Users.users.vend.getUserByProviderId(provider = DAuthValue, idGivenByProvider = username) case _ => None } diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 511af54ee..bf686dc7c 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -181,7 +181,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ def hasGatewayHeader(authorization: Box[String]) = hasHeader("GatewayLogin", authorization) - def hasDAuthHeader(authorization: Box[String]) = hasHeader("DAuthLogin", authorization) + def hasDAuthHeader(authorization: Box[String]) = hasHeader("DAuth", authorization) /** * Helper function which tells us does an "Authorization" request header field has the Type of an authentication scheme @@ -2249,7 +2249,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ /** * Defines DAuth Custom Response Header. */ - val DAuthResponseHeaderName = "DAuthLogin" + val DAuthResponseHeaderName = "DAuth" /** * Set value of DAuth Custom Response Header. */ @@ -2263,7 +2263,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case _ => Nil } } - def getDAuthLoginJwt(): Option[String] = { + def getDAuthJwt(): Option[String] = { getDAuthResponseHeader() match { case x :: Nil => Some(x._2) @@ -2735,22 +2735,22 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case _ => Future { (Failure(ErrorMessages.GatewayLoginUnknownError), None) } } - } // DAuthLogin Login + } // DAuth Login else if (getPropsAsBoolValue("allow_dauth_login", false) && hasDAuthHeader(cc.authReqHeaderField)) { logger.info("allow_dauth_login-getRemoteIpAddress: " + remoteIpAddress ) APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature - val (httpCode, message, parameters) = DAuthLogin.validator(s.request) + val (httpCode, message, parameters) = DAuth.validator(s.request) httpCode match { case 200 => - val payload = DAuthLogin.parseJwt(parameters) + val payload = DAuth.parseJwt(parameters) payload match { case Full(payload) => - DAuthLogin.getOrCreateResourceUserFuture(payload: String, Some(cc)) map { + DAuth.getOrCreateResourceUserFuture(payload: String, Some(cc)) map { case Full((u,callContext)) => // Authentication is successful - val consumer = DAuthLogin.getOrCreateConsumer(payload, u) - val jwt = DAuthLogin.createJwt(payload) - val callContextUpdated = ApiSession.updateCallContext(DAuthLoginResponseHeader(Some(jwt)), callContext) + val consumer = DAuth.getOrCreateConsumer(payload, u) + val jwt = DAuth.createJwt(payload) + val callContextUpdated = ApiSession.updateCallContext(DAuthResponseHeader(Some(jwt)), callContext) (Full(u), callContextUpdated.map(_.copy(consumer=consumer, user = Full(u)))) case Failure(msg, t, c) => (Failure(msg, t, c), None) @@ -2760,19 +2760,19 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case Failure(msg, t, c) => Future { (Failure(msg, t, c), None) } case _ => - Future { (Failure(ErrorMessages.DAuthLoginUnknownError), None) } + Future { (Failure(ErrorMessages.DAuthUnknownError), None) } } case _ => Future { (Failure(message), None) } } case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == false) => // All other addresses will be rejected - Future { (Failure(ErrorMessages.DAuthLoginWhiteListAddresses), None) } + Future { (Failure(ErrorMessages.DAuthWhiteListAddresses), None) } case Empty => - Future { (Failure(ErrorMessages.DAuthLoginHostPropertyMissing), None) } // There is no dauth.host in props file + Future { (Failure(ErrorMessages.DAuthHostPropertyMissing), None) } // There is no dauth.host in props file case Failure(msg, t, c) => Future { (Failure(msg, t, c), None) } case _ => - Future { (Failure(ErrorMessages.DAuthLoginUnknownError), None) } + Future { (Failure(ErrorMessages.DAuthUnknownError), None) } } } else if(Option(cc).flatMap(_.user).isDefined) { diff --git a/obp-api/src/main/scala/code/api/util/ApiSession.scala b/obp-api/src/main/scala/code/api/util/ApiSession.scala index af9e7c08b..889397845 100644 --- a/obp-api/src/main/scala/code/api/util/ApiSession.scala +++ b/obp-api/src/main/scala/code/api/util/ApiSession.scala @@ -5,7 +5,7 @@ import java.util.{Date, UUID} import code.api.JSONFactoryGateway.PayloadOfJwtJSON import code.api.oauth1a.OauthParams._ import code.api.util.APIUtil._ -import code.api.util.AuthenticationType.{Anonymous, DirectLogin, GatewayLogin, DAuthLogin, OAuth2_OIDC, OAuth2_OIDC_FAPI} +import code.api.util.AuthenticationType.{Anonymous, DirectLogin, GatewayLogin, DAuth, OAuth2_OIDC, OAuth2_OIDC_FAPI} import code.api.util.ErrorMessages.{BankAccountNotFound, UserNotLoggedIn} import code.api.util.RateLimitingJson.CallLimit import code.context.UserAuthContextProvider @@ -26,8 +26,8 @@ import scala.collection.immutable.List case class CallContext( gatewayLoginRequestPayload: Option[PayloadOfJwtJSON] = None, //Never update these values inside the case class !!! gatewayLoginResponseHeader: Option[String] = None, - dauthLoginRequestPayload: Option[JSONFactoryDAuth.PayloadOfJwtJSON] = None, //Never update these values inside the case class !!! - dauthLoginResponseHeader: Option[String] = None, + dauthRequestPayload: Option[JSONFactoryDAuth.PayloadOfJwtJSON] = None, //Never update these values inside the case class !!! + dauthResponseHeader: Option[String] = None, spelling: Option[String] = None, user: Box[User] = Empty, consumer: Box[Consumer] = Empty, @@ -141,7 +141,7 @@ case class CallContext( if(hasGatewayHeader(authReqHeaderField)) { GatewayLogin } else if(hasDAuthHeader(authReqHeaderField)) { // DAuth Login - DAuthLogin + DAuth } else if(has2021DirectLoginHeader(requestHeaders)) { // Direct Login DirectLogin } else if(hasDirectLoginHeader(authReqHeaderField)) { // Direct Login Deprecated @@ -166,7 +166,7 @@ object AuthenticationType extends OBPEnumeration[AuthenticationType]{ override def toString: String = "OAuth1.0a" } object GatewayLogin extends AuthenticationType - object DAuthLogin extends AuthenticationType + object DAuth extends AuthenticationType object OAuth2_OIDC extends AuthenticationType object OAuth2_OIDC_FAPI extends AuthenticationType object Anonymous extends AuthenticationType @@ -202,8 +202,8 @@ case class CallContextLight(gatewayLoginRequestPayload: Option[PayloadOfJwtJSON] trait LoginParam case class GatewayLoginRequestPayload(jwtPayload: Option[PayloadOfJwtJSON]) extends LoginParam case class GatewayLoginResponseHeader(jwt: Option[String]) extends LoginParam -case class DAuthLoginRequestPayload(jwtPayload: Option[JSONFactoryDAuth.PayloadOfJwtJSON]) extends LoginParam -case class DAuthLoginResponseHeader(jwt: Option[String]) extends LoginParam +case class DAuthRequestPayload(jwtPayload: Option[JSONFactoryDAuth.PayloadOfJwtJSON]) extends LoginParam +case class DAuthResponseHeader(jwt: Option[String]) extends LoginParam case class Spelling(spelling: Box[String]) @@ -246,9 +246,9 @@ object ApiSession { def updateCallContext(jwt: LoginParam, cnt: Option[CallContext]): Option[CallContext] = { jwt match { - case GatewayLoginRequestPayload(None) | DAuthLoginRequestPayload(None) => + case GatewayLoginRequestPayload(None) | DAuthRequestPayload(None) => cnt - case GatewayLoginResponseHeader(None) | DAuthLoginResponseHeader(None) => + case GatewayLoginResponseHeader(None) | DAuthResponseHeader(None) => cnt case GatewayLoginRequestPayload(Some(jwtPayload)) => cnt match { @@ -264,19 +264,19 @@ object ApiSession { case None => Some(CallContext(gatewayLoginRequestPayload = None, gatewayLoginResponseHeader = Some(j), spelling = None)) } - case DAuthLoginRequestPayload(Some(jwtPayload)) => + case DAuthRequestPayload(Some(jwtPayload)) => cnt match { case Some(v) => - Some(v.copy(dauthLoginRequestPayload = Some(jwtPayload))) + Some(v.copy(dauthRequestPayload = Some(jwtPayload))) case None => - Some(CallContext(dauthLoginRequestPayload = Some(jwtPayload), dauthLoginResponseHeader = None, spelling = None)) + Some(CallContext(dauthRequestPayload = Some(jwtPayload), dauthResponseHeader = None, spelling = None)) } - case DAuthLoginResponseHeader(Some(j)) => + case DAuthResponseHeader(Some(j)) => cnt match { case Some(v) => - Some(v.copy(dauthLoginResponseHeader = Some(j))) + Some(v.copy(dauthResponseHeader = Some(j))) case None => - Some(CallContext(dauthLoginRequestPayload = None, dauthLoginResponseHeader = Some(j), spelling = None)) + Some(CallContext(dauthRequestPayload = None, dauthResponseHeader = Some(j), spelling = None)) } } } diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index a8b878d63..d7b512dfe 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -182,17 +182,12 @@ object ErrorMessages { val UserIsDeleted = "OBP-20064: The user is deleted!" - val DAuthLoginCannotGetOrCreateUser = "OBP-20065: Cannot get or create user during DAuthLogin process." - val DAuthLoginMissingParameters = "OBP-20066: These DAuthLogin parameters are missing: " - val DAuthLoginUnknownError = "OBP-20067: Unknown Gateway login error." - val DAuthLoginHostPropertyMissing = "OBP-20068: Property gateway.host is not defined." - val DAuthLoginWhiteListAddresses = "OBP-20069: Gateway login can be done only from allowed addresses." - val DAuthLoginJwtTokenIsNotValid = "OBP-20070: The JWT is corrupted/changed during a transport." - val DAuthLoginCannotExtractJwtToken = "OBP-20071: Header, Payload and Signature cannot be extracted from the JWT." - val DAuthLoginNoNeedToCallCbs = "OBP-20072: There is no need to call CBS" - val DAuthLoginCannotFindUser = "OBP-20073: User cannot be found. Please initiate CBS communication in order to create it." - val DAuthLoginCannotGetCbsToken = "OBP-20074: Cannot get the CBSToken response from South side" - val DAuthLoginNoJwtForResponse = "OBP-20075: There is no useful value for JWT." + val DAuthCannotGetOrCreateUser = "OBP-20065: Cannot get or create user during DAuth process." + val DAuthMissingParameters = "OBP-20066: These DAuth parameters are missing: " + val DAuthUnknownError = "OBP-20067: Unknown DAuth login error." + val DAuthHostPropertyMissing = "OBP-20068: Property dauth.host is not defined." + val DAuthWhiteListAddresses = "OBP-20069: DAuth login can be done only from allowed addresses." + val DAuthNoJwtForResponse = "OBP-20070: There is no useful value for JWT." val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " From b6f41d6362625b08507e524490c6e7b2159ef4ec Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 2 Nov 2021 13:59:41 +0100 Subject: [PATCH 100/185] feature/added the new login type: DAuth, added the username there- step2 --- obp-api/src/main/scala/code/api/dauth.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala index 7c0cc6898..88c2d6fdd 100755 --- a/obp-api/src/main/scala/code/api/dauth.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -194,7 +194,7 @@ object DAuth extends RestHelper with MdcLoggable { logger.debug("login_user_name: " + username) for { tuple <- - Users.users.vend.getOrCreateUserByProviderIdFuture(provider = DAuthValue, idGivenByProvider = username, consentId = None, name = None, email = None) map { + Users.users.vend.getOrCreateUserByProviderIdFuture(provider = DAuthValue, idGivenByProvider = username, consentId = None, name = Some(username), email = None) map { case (Full(u), _) => Full(u, callContext) // Return user case (Empty, _) => From e393ba09ebbfb2eac4f8cf3761041a6d38bb1441 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 2 Nov 2021 17:05:17 +0100 Subject: [PATCH 101/185] feature/added the new login type: tweaked the provide and username- step3 --- obp-api/src/main/scala/code/api/dauth.scala | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala index 88c2d6fdd..a8e98ae81 100755 --- a/obp-api/src/main/scala/code/api/dauth.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -64,7 +64,6 @@ object JSONFactoryDAuth { object DAuth extends RestHelper with MdcLoggable { - val DAuthValue = "DAuth" // This value is used for ResourceUser.provider and Consumer.description def createJwt(payloadAsJsonString: String) : String = { val smartContractAddress = getFieldFromPayloadJson(payloadAsJsonString, "smart_contract_address") @@ -103,7 +102,7 @@ object DAuth extends RestHelper with MdcLoggable { Full(compactRender(Extraction.decompose(jwtPayload))) case _ => logger.debug("parseJwt says: Not Full(jwtPayload)") - Failure(ErrorMessages.GatewayLoginJwtTokenIsNotValid) + Failure(ErrorMessages.DAuthJwtTokenIsNotValid) } } @@ -124,7 +123,7 @@ object DAuth extends RestHelper with MdcLoggable { Box(parse(claim.toString).extractOpt[PayloadOfJwtJSON]) case _ => logger.debug("validateJwtToken says: could not verify jwt") - Failure(ErrorMessages.GatewayLoginJwtTokenIsNotValid) + Failure(ErrorMessages.DAuthJwtTokenIsNotValid) } } } @@ -160,12 +159,13 @@ object DAuth extends RestHelper with MdcLoggable { def getOrCreateResourceUser(jwtPayload: String, callContext: Option[CallContext]) : Box[(User, Option[CallContext])] = { val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + val provider = getFieldFromPayloadJson(jwtPayload, "network_name") logger.debug("login_user_name: " + username) for { tuple <- - Users.users.vend.getUserByProviderId(provider = DAuthValue, idGivenByProvider = username).or { // Find a user + Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = username).or { // Find a user Users.users.vend.createResourceUser( // Otherwise create a new one - provider = DAuthValue, + provider = provider, providerId = Some(username), None, name = Some(username), @@ -191,10 +191,11 @@ object DAuth extends RestHelper with MdcLoggable { } def getOrCreateResourceUserFuture(jwtPayload: String, callContext: Option[CallContext]) : Future[Box[(User, Option[CallContext])]] = { val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + val provider = getFieldFromPayloadJson(jwtPayload, "network_name") logger.debug("login_user_name: " + username) for { tuple <- - Users.users.vend.getOrCreateUserByProviderIdFuture(provider = DAuthValue, idGivenByProvider = username, consentId = None, name = Some(username), email = None) map { + Users.users.vend.getOrCreateUserByProviderIdFuture(provider = provider, idGivenByProvider = username, consentId = None, name = Some(username), email = None) map { case (Full(u), _) => Full(u, callContext) // Return user case (Empty, _) => @@ -212,6 +213,7 @@ object DAuth extends RestHelper with MdcLoggable { def getOrCreateConsumer(jwtPayload: String, u: User) : Box[Consumer] = { val consumerId = getFieldFromPayloadJson(jwtPayload, "consumer_id") val consumerName = getFieldFromPayloadJson(jwtPayload, "msg_sender") + val DAuthValue = "DAuth" // This value is used for Consumer.description logger.debug("app_id: " + consumerId) logger.debug("app_name: " + consumerName) Consumers.consumers.vend.getOrCreateConsumer( @@ -321,8 +323,9 @@ object DAuth extends RestHelper with MdcLoggable { payload match { case Full(payload) => val username = getFieldFromPayloadJson(payload, "smart_contract_address") + val provider = getFieldFromPayloadJson(payload, "network_name") logger.debug("username: " + username) - Users.users.vend.getUserByProviderId(provider = DAuthValue, idGivenByProvider = username) + Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = username) case _ => None } From 4926e58abeb55aeb3039d972ed57189e6c43a769 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 2 Nov 2021 17:05:40 +0100 Subject: [PATCH 102/185] feature/added the new login type: added the dauth tests- step4 --- .../scala/code/api/util/ErrorMessages.scala | 1 + .../src/test/scala/code/api/dauthTest.scala | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 obp-api/src/test/scala/code/api/dauthTest.scala diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index d7b512dfe..6bc843ad5 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -188,6 +188,7 @@ object ErrorMessages { val DAuthHostPropertyMissing = "OBP-20068: Property dauth.host is not defined." val DAuthWhiteListAddresses = "OBP-20069: DAuth login can be done only from allowed addresses." val DAuthNoJwtForResponse = "OBP-20070: There is no useful value for JWT." + val DAuthJwtTokenIsNotValid = "OBP-20071: The JWT is corrupted/changed during a transport." val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " diff --git a/obp-api/src/test/scala/code/api/dauthTest.scala b/obp-api/src/test/scala/code/api/dauthTest.scala new file mode 100644 index 000000000..e775244d1 --- /dev/null +++ b/obp-api/src/test/scala/code/api/dauthTest.scala @@ -0,0 +1,109 @@ +package code.api + +import code.api.util.APIUtil.OAuth._ +import code.api.util.ErrorMessages +import code.setup.{DefaultUsers, PropsReset, ServerSetup} +import org.scalatest._ + +class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with PropsReset{ + + + setPropsValues("allow_dauth_login" -> "true") + setPropsValues("dauth.host" -> "127.0.0.1") + setPropsValues("jwt.token_secret"->"secretsecretsecretstsecretssssss") + + val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") + /* Payload data. verified by wrong secret "123" -- show : DAuthJwtTokenIsNotValid + { + "smart_contract_address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "network_name": "ETHEREUM", + "msg_sender": "0xe90980927f1725E7734CE288F8367e1Bb143E90fhku767", + "consumer_id": "0x19255a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", + "time_stamp": "2018-08-20T14:13:40Z", + "caller_request_id": "0Xe876987694328763492876348928736497869273649" +} + */ + val invalidSecretJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfaWQiOiIweDE5MjU1YTRlYzMxZTg5Y2VhNTRkMWYxMjVkYjc1MzZlODc0YWI0YTk2YjRkNGY2NDM4NjY4YjZiYjEwYTZhZGIiLCJ0aW1lX3N0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJjYWxsZXJfcmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.5t1bolx13gCBSyvbTzv_QWP1tFkN0m_Sv727bB1QZuw" + + /* Payload data. verified by correct secret "secretsecretsecretstsecretssssss" + { + "smart_contract_address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F05124", + "network_name": "ETHEREUM", + "msg_sender": "0xe90980927f1725E7734CE288F8367e1Bb143E90fhku767", + "consumer_id": "0x19255a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", + "time_stamp": "2018-08-20T14:13:40Z", + "caller_request_id": "0Xe876987694328763492876348928736497869273649" +} + */ + val jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3Z" + + "TFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhG" + + "ODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfaWQiOiIweDE5MjU1YTRlYzMxZTg5Y2VhNTRkMWYxMjVkYjc1MzZlODc0YWI0YTk2YjRkNGY2" + + "NDM4NjY4YjZiYjEwYTZhZGIiLCJ0aW1lX3N0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJjYWxsZXJfcmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5" + + "NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.us2wjYUwiQHYdmU9JBNEMz8rc8qVGzY6bDNeknC3HMo" + + val invalidJwt = ("Authorization", ("DAuth token=%s").format(invalidSecretJwt)) + val validJwt = ("Authorization", ("DAuth token=%s").format(jwt)) + val missingParameterToken = ("Authorization", ("DAuth wrong_parameter_name=%s").format(jwt)) + + def dauthRequest = baseRequest / "obp" / "v2.0.0" / "users" /"current" <@ (user1) + def dauthNonBlockingRequest = baseRequest / "obp" / "v3.0.0" / "users" / "current" <@ (user1) + + feature("DAuth in a BLOCKING way") { + + scenario("Missing parameter token in a blocking way") { + When("We try to login without parameter token in a Header") + val response = makeGetRequest(dauthRequest, List(missingParameterToken)) + Then("We should get a 400 - Bad Request") + logger.debug("-----------------------------------------") + logger.debug(response) + logger.debug("-----------------------------------------") + response.code should equal(400) + response.toString contains (ErrorMessages.DAuthMissingParameters) should be (true) + + When("We try to login with an invalid JWT") + val responseInvalid = makeGetRequest(dauthRequest, List(invalidJwt)) + Then("We should get a 400 - Bad Request") + logger.debug("-----------------------------------------") + logger.debug("responseInvalid response: "+responseInvalid) + logger.debug("-----------------------------------------") + responseInvalid.code should equal(400) + responseInvalid.toString contains (ErrorMessages.DAuthJwtTokenIsNotValid) should be (true) + + When("We try to login with an valid JWT") + val responseValidJwt = makeGetRequest(dauthRequest, List(validJwt)) + logger.debug("-----------------------------------------") + logger.debug("responseValidJwt response: "+responseValidJwt) + logger.debug("-----------------------------------------") + responseValidJwt.code should equal(200) + + When("We try to login without parameter token in a Header") + val responseNonBlocking = makeGetRequest(dauthNonBlockingRequest, List(missingParameterToken)) + Then("We should get a 400 - Bad Request") + logger.debug("-----------------------------------------") + logger.debug("responseNonBlocking: "+ responseNonBlocking) + logger.debug("-----------------------------------------") + responseNonBlocking.code should equal(401) + responseNonBlocking.toString contains (ErrorMessages.DAuthMissingParameters) should be (true) + + When("We try to login with an invalid JWT") + val responseNonBlockingInvalid = makeGetRequest(dauthNonBlockingRequest, List(invalidJwt)) + Then("We should get a 400 - Bad Request") + logger.debug("-----------------------------------------") + logger.debug("responseNonBlockingInvalid responseNonBlocking: "+responseNonBlockingInvalid) + logger.debug("-----------------------------------------") + responseNonBlockingInvalid.code should equal(401) + responseNonBlockingInvalid.toString contains (ErrorMessages.DAuthJwtTokenIsNotValid) should be (true) + + When("We try to login with an valid JWT") + val responseNonBlockingValidJwt = makeGetRequest(dauthNonBlockingRequest, List(validJwt)) + logger.debug("-----------------------------------------") + logger.debug("responseNonBlockingValidJwt responseNonBlocking: "+responseNonBlockingValidJwt) + logger.debug("-----------------------------------------") + responseValidJwt.code should equal(200) + } + + } + + + +} \ No newline at end of file From c3d4193dec71202d9bf53f99568542c185f840c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 2 Nov 2021 17:06:01 +0100 Subject: [PATCH 103/185] feature/OIDC - Show username in casse email is empty --- obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index 121b96988..c0439ec23 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -487,9 +487,9 @@ import net.liftweb.util.Helpers._ */ def getCurrentUserUsername: String = { getCurrentUser match { - case Full(user) if user.provider.contains("google") => user.emailAddress - case Full(user) if user.provider.contains("yahoo") => user.emailAddress - case Full(user) if user.provider.contains("microsoft") => user.emailAddress + case Full(user) if user.provider.contains("google") && !user.emailAddress.isEmpty => user.emailAddress + case Full(user) if user.provider.contains("yahoo") && !user.emailAddress.isEmpty => user.emailAddress + case Full(user) if user.provider.contains("microsoft") && !user.emailAddress.isEmpty => user.emailAddress case Full(user) => user.name case _ => "" //TODO need more error handling for different user cases } From 3a965803b56768d4dad135635bb898fd4ccd01d9 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 2 Nov 2021 17:25:11 +0100 Subject: [PATCH 104/185] feature/added the migration code for UserAuthContext Column KeyAndValue length to 4000 --- .../code/api/util/migration/Migration.scala | 13 ++++ ...igrationOfUserAuthContextFieldLength.scala | 63 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 obp-api/src/main/scala/code/api/util/migration/MigrationOfUserAuthContextFieldLength.scala diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index f2d37d31b..0bc5a7c58 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -87,6 +87,7 @@ object Migration extends MdcLoggable { alterColumnNameAtProductFee(startedBeforeSchemifier) addFastFirehoseAccountsView(startedBeforeSchemifier) addFastFirehoseAccountsMaterializedView(startedBeforeSchemifier) + alterUserAuthContextColumnKeyAndValueLength(startedBeforeSchemifier) } private def dummyScript(): Boolean = { @@ -321,6 +322,18 @@ object Migration extends MdcLoggable { } } + private def alterUserAuthContextColumnKeyAndValueLength(startedBeforeSchemifier: Boolean): Boolean = { + if(startedBeforeSchemifier == true) { + logger.warn(s"Migration.database.alterUserAuthContextColumnKeyAndValueLength(true) cannot be run before Schemifier.") + true + } else { + val name = nameOf(alterUserAuthContextColumnKeyAndValueLength(startedBeforeSchemifier)) + runOnce(name) { + MigrationOfUserAuthContextFieldLength.alterColumnKeyAndValueLength(name) + } + } + } + } /** diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfUserAuthContextFieldLength.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfUserAuthContextFieldLength.scala new file mode 100644 index 000000000..d38aa41a1 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfUserAuthContextFieldLength.scala @@ -0,0 +1,63 @@ +package code.api.util.migration + +import java.time.format.DateTimeFormatter +import java.time.{ZoneId, ZonedDateTime} +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.context.MappedUserAuthContext +import net.liftweb.common.Full +import net.liftweb.mapper.{DB, Schemifier} +import net.liftweb.util.DefaultConnectionIdentifier + +object MigrationOfUserAuthContextFieldLength { + + val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1) + val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'") + + def alterColumnKeyAndValueLength(name: String): Boolean = { + DbFunction.tableExists(MappedUserAuthContext, (DB.use(DefaultConnectionIdentifier){ conn => conn})) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _, DB.use(DefaultConnectionIdentifier){ conn => conn}) { + APIUtil.getPropsValue("db.driver") match { + case Full(value) if value.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ + |ALTER TABLE MappedUserAuthContext ALTER COLUMN mKey varchar(4000); + |ALTER TABLE MappedUserAuthContext ALTER COLUMN mValue varchar(4000); + |""".stripMargin + case _ => + () => + """ + |ALTER TABLE MappedUserAuthContext ALTER COLUMN mKey type varchar(4000); + |ALTER TABLE MappedUserAuthContext ALTER COLUMN mValue type varchar(4000); + |""".stripMargin + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${MappedUserAuthContext._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } +} \ No newline at end of file From 7452cde75e2d17a259c6fefe922a31fb65ed15a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 3 Nov 2021 16:48:57 +0100 Subject: [PATCH 105/185] feature/Add endpoint createHistoricalTransactionAtBank v4.0.0 --- .../SwaggerDefinitionsJSON.scala | 14 +- .../main/scala/code/api/util/ApiRole.scala | 3 + .../scala/code/api/v4_0_0/APIMethods400.scala | 155 ++++++++++++++++++ .../code/api/v4_0_0/JSONFactory4.0.0.scala | 59 ++++++- 4 files changed, 229 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 7fb68a87c..fd0d420d4 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -1,6 +1,7 @@ package code.api.ResourceDocs1_4_0 import java.util.Date + import code.api.Constant._ import code.api.Constant import code.api.UKOpenBanking.v2_0_0.JSONFactory_UKOpenBanking_200 @@ -15,7 +16,7 @@ import code.api.v3_0_0.JSONFactory300.createBranchJsonV300 import code.api.v3_0_0.custom.JSONFactoryCustom300 import code.api.v3_0_0.{LobbyJsonV330, _} import code.api.v3_1_0.{AccountBalanceV310, AccountsBalancesV310Json, BadLoginStatusJson, ContactDetailsJson, CustomerWithAttributesJsonV310, InviteeJson, ObpApiLoopbackJson, PhysicalCardWithAttributesJsonV310, PutUpdateCustomerEmailJsonV310, _} -import code.api.v4_0_0.{BankAttributeBankResponseJsonV400, FastFirehoseAccountsJsonV400, _} +import code.api.v4_0_0.{BankAttributeBankResponseJsonV400, FastFirehoseAccountsJsonV400, PostHistoricalTransactionAtBankJson, _} import code.api.v3_1_0.{AccountBalanceV310, AccountsBalancesV310Json, BadLoginStatusJson, ContactDetailsJson, InviteeJson, ObpApiLoopbackJson, PhysicalCardWithAttributesJsonV310, PutUpdateCustomerEmailJsonV310, _} import code.branches.Branches.{Branch, DriveUpString, LobbyString} import code.consent.ConsentStatus @@ -33,6 +34,7 @@ import com.openbankproject.commons.model.{UserAuthContextUpdateStatus, ViewBasic import com.openbankproject.commons.util.{ApiVersion, FieldNameApiVersions, ReflectUtils, RequiredArgs, RequiredInfo} import net.liftweb.json import java.net.URLEncoder + import code.endpointMapping.EndpointMappingCommons import scala.collection.immutable.List @@ -3874,6 +3876,16 @@ object SwaggerDefinitionsJSON { completed= DateWithSecondsExampleString, `type`= SANDBOX_TAN.toString, charge_policy= "SHARED" + ) + val postHistoricalTransactionAtBankJson = PostHistoricalTransactionAtBankJson( + from_account_id = "", + to_account_id = "", + value = amountOfMoneyJsonV121, + description = "this is for work", + posted = DateWithSecondsExampleString, + completed= DateWithSecondsExampleString, + `type`= SANDBOX_TAN.toString, + charge_policy= "SHARED" ) val postHistoricalTransactionResponseJson = PostHistoricalTransactionResponseJson( diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 1becd2472..2dc44a533 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -821,6 +821,9 @@ object ApiRole { case class CanGetBankLevelEndpointTag(requiresBankId: Boolean = true) extends ApiRole lazy val canGetBankLevelEndpointTag = CanGetBankLevelEndpointTag() + case class CanCreateHistoricalTransactionAtBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canCreateHistoricalTransactionAtBank = CanCreateHistoricalTransactionAtBank() + private val dynamicApiRoles = new ConcurrentHashMap[String, ApiRole] private case class DynamicApiRole(role: String, requiresBankId: Boolean = false) extends ApiRole{ diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index ef83826cc..4c61c0ed2 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -79,6 +79,7 @@ import net.liftweb.util.{Helpers, Mailer, StringHelpers} import org.apache.commons.collections4.CollectionUtils import org.apache.commons.lang3.StringUtils import java.net.URLEncoder +import java.text.SimpleDateFormat import java.util.{Calendar, Date} import scala.collection.immutable.{List, Nil} @@ -4714,6 +4715,160 @@ trait APIMethods400 { } } + staticResourceDocs += ResourceDoc( + createHistoricalTransactionAtBank, + implementedInApiVersion, + nameOf(createHistoricalTransactionAtBank), + "POST", + "/banks/BANK_ID/management/historical/transactions", + "Create Historical Transactions ", + s""" + |Import the historical transactions. + | + |The fields bank_id, account_id, counterparty_id in the json body are all optional ones. + |It support transfer money from account to account, account to counterparty and counterparty to counterparty + |Both bank_id + account_id and counterparty_id can identify the account, so OBP only need one of them to make the payment. + |So: + |When you need the account to account, just omit counterparty_id field.eg: + |{ + | "from": { + | "bank_id": "gh.29.uk", + | "account_id": "1ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", + | }, + | "to": { + | "bank_id": "gh.29.uk", + | "account_id": "2ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", + | }, + | "value": { + | "currency": "GBP", + | "amount": "10" + | }, + | "description": "this is for work", + | "posted": "2017-09-19T02:31:05Z", + | "completed": "2017-09-19T02:31:05Z", + | "type": "SANDBOX_TAN", + | "charge_policy": "SHARED" + |} + | + |When you need the counterparty to counterparty, need to omit bank_id and account_id field.eg: + |{ + | "from": { + | "counterparty_id": "f6392b7d-4218-45ea-b9a7-eaa71c0202f9" + | }, + | "to": { + | "counterparty_id": "26392b7d-4218-45ea-b9a7-eaa71c0202f9" + | }, + | "value": { + | "currency": "GBP", + | "amount": "10" + | }, + | "description": "this is for work", + | "posted": "2017-09-19T02:31:05Z", + | "completed": "2017-09-19T02:31:05Z", + | "type": "SANDBOX_TAN", + | "charge_policy": "SHARED" + |} + | + |or, you can counterparty to account + |{ + | "from": { + | "counterparty_id": "f6392b7d-4218-45ea-b9a7-eaa71c0202f9" + | }, + | "to": { + | "bank_id": "gh.29.uk", + | "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", + | }, + | "value": { + | "currency": "GBP", + | "amount": "10" + | }, + | "description": "this is for work", + | "posted": "2017-09-19T02:31:05Z", + | "completed": "2017-09-19T02:31:05Z", + | "type": "SANDBOX_TAN", + | "charge_policy": "SHARED" + |} + | + |This call is experimental. + """.stripMargin, + postHistoricalTransactionAtBankJson, + postHistoricalTransactionResponseJson, + List( + InvalidJsonFormat, + BankNotFound, + AccountNotFound, + CounterpartyNotFoundByCounterpartyId, + InvalidNumber, + NotPositiveAmount, + InvalidTransactionRequestCurrency, + UnknownError + ), + List(apiTagTransactionRequest, apiTagNewStyle), + Some(List(canCreateHistoricalTransactionAtBank)) + ) + + + lazy val createHistoricalTransactionAtBank : OBPEndpoint = { + case "banks" :: BankId(bankId) :: "management" :: "historical" :: "transactions" :: Nil JsonPost json -> _ => { + cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, ApiRole.canCreateHistoricalTransactionAtBank, callContext) + + // Check the input JSON format, here is just check the common parts of all four types + transDetailsJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostHistoricalTransactionJson ", 400, callContext) { + json.extract[PostHistoricalTransactionAtBankJson] + } + (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, AccountId(transDetailsJson.from_account_id), callContext) + (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, AccountId(transDetailsJson.to_account_id), callContext) + amountNumber <- NewStyle.function.tryons(s"$InvalidNumber Current input is ${transDetailsJson.value.amount} ", 400, callContext) { + BigDecimal(transDetailsJson.value.amount) + } + _ <- Helper.booleanToFuture(s"${NotPositiveAmount} Current input is: '${amountNumber}'", cc=callContext) { + amountNumber > BigDecimal("0") + } + posted <- NewStyle.function.tryons(s"$InvalidDateFormat Current `posted` field is ${transDetailsJson.posted}. Please use this format ${DateWithSecondsFormat.toPattern}! ", 400, callContext) { + new SimpleDateFormat(DateWithSeconds).parse(transDetailsJson.posted) + } + completed <- NewStyle.function.tryons(s"$InvalidDateFormat Current `completed` field is ${transDetailsJson.completed}. Please use this format ${DateWithSecondsFormat.toPattern}! ", 400, callContext) { + new SimpleDateFormat(DateWithSeconds).parse(transDetailsJson.completed) + } + // Prevent default value for transaction request type (at least). + _ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) { + isValidCurrencyISOCode(transDetailsJson.value.currency) + } + amountOfMoneyJson = AmountOfMoneyJsonV121(transDetailsJson.value.currency, transDetailsJson.value.amount) + chargePolicy = transDetailsJson.charge_policy + //There is no constraint for the type at the moment + transactionType = transDetailsJson.`type` + (transactionId, callContext) <- NewStyle.function.makeHistoricalPayment( + fromAccount, + toAccount, + posted, + completed, + amountNumber, + transDetailsJson.value.currency, + transDetailsJson.description, + transactionType, + chargePolicy, + callContext + ) + } yield { + (JSONFactory400.createPostHistoricalTransactionResponseJson( + bankId, + transactionId, + fromAccount.accountId, + toAccount.accountId, + value= amountOfMoneyJson, + description = transDetailsJson.description, + posted, + completed, + transactionRequestType = transactionType, + chargePolicy =transDetailsJson.charge_policy), HttpCode.`201`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( getTransactionRequest, diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index 90ef594fd..0c54094a0 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -42,7 +42,7 @@ import code.api.v2_2_0.CounterpartyMetadataJson import code.api.v3_0_0.JSONFactory300._ import code.api.v3_0_0._ import code.api.v3_1_0.JSONFactory310.{createAccountAttributeJson, createProductAttributesJson} -import code.api.v3_1_0.{AccountAttributeResponseJson, ProductAttributeResponseWithoutBankIdJson, RedisCallLimitJson} +import code.api.v3_1_0.{AccountAttributeResponseJson, PostHistoricalTransactionResponseJson, ProductAttributeResponseWithoutBankIdJson, RedisCallLimitJson} import code.apicollection.ApiCollectionTrait import code.apicollectionendpoint.ApiCollectionEndpointTrait import code.atms.Atms.Atm @@ -128,6 +128,31 @@ case class TransactionRequestWithChargeJSON400( challenges: List[ChallengeJsonV400], charge : TransactionRequestChargeJsonV200 ) +case class PostHistoricalTransactionAtBankJson( + from_account_id: String, + to_account_id: String, + value: AmountOfMoneyJsonV121, + description: String, + posted: String, + completed: String, + `type`: String, + charge_policy: String + ) +case class HistoricalTransactionAccountJsonV400( + bank_id: String, + account_id : String + ) +case class PostHistoricalTransactionResponseJsonV400( + transaction_id: String, + from: HistoricalTransactionAccountJsonV400, + to: HistoricalTransactionAccountJsonV400, + value: AmountOfMoneyJsonV121, + description: String, + posted: Date, + completed: Date, + transaction_request_type: String, + charge_policy: String + ) case class PostResetPasswordUrlJsonV400(username: String, email: String, user_id: String) case class ResetPasswordUrlJsonV400(reset_password_url: String) @@ -1730,5 +1755,37 @@ object JSONFactory400 { )))) ) } + + + + def createPostHistoricalTransactionResponseJson( + bankId: BankId, + transactionId: TransactionId, + fromAccountId: AccountId, + toAccountId: AccountId, + value: AmountOfMoneyJsonV121, + description: String, + posted: Date, + completed: Date, + transactionRequestType: String, + chargePolicy: String + ) : PostHistoricalTransactionResponseJsonV400 = { + PostHistoricalTransactionResponseJsonV400( + transaction_id = transactionId.value, + from = HistoricalTransactionAccountJsonV400(bankId.value, fromAccountId.value), + to = HistoricalTransactionAccountJsonV400(bankId.value, toAccountId.value), + value: AmountOfMoneyJsonV121, + description: String, + posted: Date, + completed: Date, + transaction_request_type = transactionRequestType, + chargePolicy: String + ) + } + + + + + } From 52be51366833fa61eac19037886d4310ef36269e Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 4 Nov 2021 11:51:46 +0100 Subject: [PATCH 106/185] feature/rename allow_dauth_login -> allow_dauth --- obp-api/src/main/resources/props/sample.props.template | 2 +- obp-api/src/main/scala/code/api/OBPRestHelper.scala | 4 ++-- obp-api/src/main/scala/code/api/util/APIUtil.scala | 4 ++-- obp-api/src/test/scala/code/api/dauthTest.scala | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 54ef0d035..0232830a4 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -693,7 +693,7 @@ autocomplete_at_login_form_enabled=false # -- DAuth login -------------------------------------- # Enable/Disable DAuth communication at all # In case isn't defined default value is false -# allow_dauth_login=false +# allow_dauth=false # Define comma separated list of allowed IP addresses # dauth.host=127.0.0.1 diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 6475ae593..475374a55 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -402,8 +402,8 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { Failure(ErrorMessages.GatewayLoginUnknownError) } } - else if (APIUtil.getPropsAsBoolValue("allow_dauth_login", false) && hasDAuthHeader(authorization)) { - logger.info("allow_dauth_login-getRemoteIpAddress: " + remoteIpAddress ) + else if (APIUtil.getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(authorization)) { + logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature val s = S diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index bf686dc7c..05daae483 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2736,8 +2736,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ Future { (Failure(ErrorMessages.GatewayLoginUnknownError), None) } } } // DAuth Login - else if (getPropsAsBoolValue("allow_dauth_login", false) && hasDAuthHeader(cc.authReqHeaderField)) { - logger.info("allow_dauth_login-getRemoteIpAddress: " + remoteIpAddress ) + else if (getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.authReqHeaderField)) { + logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature val (httpCode, message, parameters) = DAuth.validator(s.request) diff --git a/obp-api/src/test/scala/code/api/dauthTest.scala b/obp-api/src/test/scala/code/api/dauthTest.scala index e775244d1..cacd5d30b 100644 --- a/obp-api/src/test/scala/code/api/dauthTest.scala +++ b/obp-api/src/test/scala/code/api/dauthTest.scala @@ -8,7 +8,7 @@ import org.scalatest._ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with PropsReset{ - setPropsValues("allow_dauth_login" -> "true") + setPropsValues("allow_dauth" -> "true") setPropsValues("dauth.host" -> "127.0.0.1") setPropsValues("jwt.token_secret"->"secretsecretsecretstsecretssssss") From 5768a46f0ff7ad956bb4b8c386fe29d7572f7374 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 4 Nov 2021 13:29:06 +0100 Subject: [PATCH 107/185] docfix/added the dauth to Glossary --- .../resources/props/sample.props.template | 6 +- .../main/scala/code/api/OBPRestHelper.scala | 1 - .../main/scala/code/api/util/Glossary.scala | 192 +++++++++++++++++- 3 files changed, 188 insertions(+), 11 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 0232830a4..074f4f42f 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -680,7 +680,7 @@ autocomplete_at_login_form_enabled=false # -- Gateway And DAuth common Setting-------------------------------------- # Define secret used to validate JWT token -# jwt.token_secret=secretsecretsecretstsecretssssss +# jwt.token_secret=your-at-least-256-bit-secret-token # -- Gateway login -------------------------------------- # Enable/Disable Gateway communication at all @@ -690,14 +690,14 @@ autocomplete_at_login_form_enabled=false # gateway.host=127.0.0.1 -# -- DAuth login -------------------------------------- +# -- DAuth -------------------------------------- # Enable/Disable DAuth communication at all # In case isn't defined default value is false # allow_dauth=false # Define comma separated list of allowed IP addresses # dauth.host=127.0.0.1 -# -------------------------------------- DAuth login -- +# -------------------------------------- DAuth-- # Disable akka (Remote storage not possible) diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 475374a55..29900da1f 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -417,7 +417,6 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { DAuth.getOrCreateResourceUser(payload: String, Some(cc)) match { case Full((u, callContext)) => // Authentication is successful val consumer = DAuth.getOrCreateConsumer(payload, u) - setGatewayResponseHeader(s) {DAuth.createJwt(payload)} val jwt = DAuth.createJwt(payload) val callContextUpdated = ApiSession.updateCallContext(DAuthResponseHeader(Some(jwt)), callContext) fn(callContextUpdated.map( callContext =>callContext.copy(user = Full(u), consumer = consumer)).getOrElse(callContext.getOrElse(cc).copy(user = Full(u), consumer = consumer))) 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 4f4a54e21..a3663dd02 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -1844,7 +1844,7 @@ object Glossary extends MdcLoggable { |# Define comma separated list of allowed IP addresses |# gateway.host=127.0.0.1 |# Define secret used to validate JWT token -|# jwt.token_secret=secret +|# jwt.token_secret=your-at-least-256-bit-secret-token |# -------------------------------------- Gateway login -- |``` |Please keep in mind that property jwt.token_secret is used to validate JWT token to check it is not changed or corrupted during transport. @@ -1881,15 +1881,15 @@ object Glossary extends MdcLoggable { | base64UrlEncode(header) + "." + | base64UrlEncode(payload), | -|) secret +|) your-at-least-256-bit-secret-token |``` | |Here is the above example token: | |``` |eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. -|AS8D76F7A89S87D6F7A9SD876FA789SD78F6A7S9D78F6AS79DF87A6S7D9F7A6S7D9F78A6SD798F78679D786S789D78F6A7S9D78F6AS79DF876A7S89DF786AS9D87F69AS7D6FN1bWVyIn0. -|KEuvjv3dmwkOhQ3JJ6dIShK8CG_fd2REApOGn1TRmgU +|eyJsb2dpbl91c2VyX25hbWUiOiJ1c2VybmFtZSIsImlzX2ZpcnN0IjpmYWxzZSwiYXBwX2lkIjoiODVhOTY1ZjAtMGQ1NS00ZTBhLThiMWMtNjQ5YzRiMDFjNGZiIiwiYXBwX25hbWUiOiJHV0wiLCJ0aW1lX3N0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJjYnNfdG9rZW4iOiJ5b3VyX3Rva2VuIiwiY2JzX2lkIjoieW91cl9jYnNfaWQiLCJzZXNzaW9uX2lkIjoiMTIzNDU2Nzg5In0. +|bfWGWttEEcftiqrb71mE6Xy1tT_I-gmDPgjzvn6kC_k |``` | | @@ -1924,8 +1924,8 @@ object Glossary extends MdcLoggable { | |``` |curl -v -H 'Authorization: GatewayLogin token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. -|AS8D76F7A89S87D6F7A9SD876FA789SD78F6A7S9D78F6AS79DF87A6S7D9F7A6S7D9F78A6SD798F78679D786S789D78F6A7S9D78F6AS79DF876A7S89DF786AS9D87F69AS7D6FN1bWVyIn0. -|KEuvjv3dmwkOhQ3JJ6dIShK8CG_fd2REApOGn1TRmgU" $getServerUrl/obp/v3.0.0/users/current +|eyJsb2dpbl91c2VyX25hbWUiOiJ1c2VybmFtZSIsImlzX2ZpcnN0IjpmYWxzZSwiYXBwX2lkIjoiODVhOTY1ZjAtMGQ1NS00ZTBhLThiMWMtNjQ5YzRiMDFjNGZiIiwiYXBwX25hbWUiOiJHV0wiLCJ0aW1lX3N0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJjYnNfdG9rZW4iOiJ5b3VyX3Rva2VuIiwiY2JzX2lkIjoieW91cl9jYnNfaWQiLCJzZXNzaW9uX2lkIjoiMTIzNDU2Nzg5In0. +|bfWGWttEEcftiqrb71mE6Xy1tT_I-gmDPgjzvn6kC_k"' $getServerUrl/obp/v3.0.0/users/current |``` | | @@ -1980,7 +1980,7 @@ object Glossary extends MdcLoggable { |} | | -|token = jwt.encode(payload, 'secretsecretsecretstsecretssssss', algorithm='HS256') +|token = jwt.encode(payload, 'your-at-least-256-bit-secret-token', algorithm='HS256').decode("utf-8") |authorization = 'GatewayLogin token="{}"'.format(token) |headers = {'Authorization': authorization} |url = obp_api_host + '/obp/v4.0.0/users/current' @@ -2016,6 +2016,184 @@ object Glossary extends MdcLoggable { """) + val dauthEnabledMessage : String = if (APIUtil.getPropsAsBoolValue("allow_gateway_login", false)) + {"Note: DAuth is enabled."} else {"Note: *DAuth is NOT enabled on this instance!*"} + + + glossaryItems += GlossaryItem( + title = "DAuth", + description = + s""" + |### Introduction +| +|$dauthEnabledMessage +| +|DAuth Authorisation is made by including a specific header (see step 3 below) in any OBP REST call. +| +|Note: DAuth does *not* require an explicit POST like Direct Login to create the token. +| +|The **Gateway is responsible** for creating a token which is trusted by OBP **absolutely**! +| +|When OBP receives a token via DAuth, OBP creates or gets a user based on the username (smart_contract_address) supplied. +| +|To use DAuth: +| +|### 1) Configure OBP API to accept DAuth. +| +|Set up properties in a props file +| +|``` +|# -- DAuth -------------------------------------- +|# Define secret used to validate JWT token +|# jwt.token_secret=your-at-least-256-bit-secret-token +|# Enable/Disable DAuth communication at all +|# In case isn't defined default value is false +|# allow_dauth=false +|# Define comma separated list of allowed IP addresses +|# dauth.host=127.0.0.1 +|# -------------------------------------- DAuth-- +|``` +|Please keep in mind that property jwt.token_secret is used to validate JWT token to check it is not changed or corrupted during transport. +| +|### 2) Create / have access to a JWT +| +| +| +|HEADER:ALGORITHM & TOKEN TYPE +| +|``` +|{ +| "alg": "HS256", +| "typ": "JWT" +|} +|``` +|PAYLOAD:DATA +| +|``` +|{ +| "smart_contract_address": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", +| "network_name": "ETHEREUM", +| "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", +| "consumer_id": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", +| "time_stamp": "2021-11-04T14:13:40Z", +| "caller_request_id": "0Xe876987694328763492876348928736497869273649" +|} +|``` +|VERIFY SIGNATURE +|``` +|HMACSHA256( +| base64UrlEncode(header) + "." + +| base64UrlEncode(payload), +| +|) your-at-least-256-bit-secret-token +|``` +| +|Here is the above example token: +| +|``` +|eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. +|eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9pZCI6IjB4MTIzNGE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZV9zdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwiY2FsbGVyX3JlcXVlc3RfaWQiOiIwWGU4NzY5ODc2OTQzMjg3NjM0OTI4NzYzNDg5Mjg3MzY0OTc4NjkyNzM2NDkifQ. +|SbgXzyRNd6uLBYql_fwXi3KAWS8SaKYMHmnVFgbGRiY +|``` +| +| +| +|### 3) Try a REST call using the header +| +| +|Using your favorite http client: +| +| GET $getServerUrl/obp/v3.0.0/users/current +| +|Body +| +| Leave Empty! +| +| +|Headers: +| +| Authorization: DAuth token="your-jwt-from-step-above" +| +|Here is it all together: +| +| GET $getServerUrl/obp/v3.0.0/users/current HTTP/1.1 +| Host: localhost:8080 +| User-Agent: curl/7.47.0 +| Accept: */* +| Authorization: GatewayLogin token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9pZCI6IjB4MTIzNGE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZV9zdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwiY2FsbGVyX3JlcXVlc3RfaWQiOiIwWGU4NzY5ODc2OTQzMjg3NjM0OTI4NzYzNDg5Mjg3MzY0OTc4NjkyNzM2NDkifQ.SbgXzyRNd6uLBYql_fwXi3KAWS8SaKYMHmnVFgbGRiY" +| +|CURL example +| +|``` +|curl -v -H 'Authorization: DAuth token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9pZCI6IjB4MTIzNGE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZV9zdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwiY2FsbGVyX3JlcXVlc3RfaWQiOiIwWGU4NzY5ODc2OTQzMjg3NjM0OTI4NzYzNDg5Mjg3MzY0OTc4NjkyNzM2NDkifQ.SbgXzyRNd6uLBYql_fwXi3KAWS8SaKYMHmnVFgbGRiY"' $getServerUrl/obp/v3.0.0/users/current +|``` +| +| +|You should receive a response like: +| +|``` +|{ +| "user_id": "4c4d3175-1e5c-4cfd-9b08-dcdc209d8221", +| "email": "", +| "provider_id": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", +| "provider": "ETHEREUM", +| "username": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", +| "entitlements": { +| "list": [] +| } +|} +|``` +| +|### Example python script +|``` +|import jwt +|from datetime import datetime, timezone +|import requests +| +|env = 'local' +|DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' +| +|obp_api_host = '$getServerUrl' +|payload = { +| "smart_contract_address": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", +| "network_name": "ETHEREUM", +| "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", +| "consumer_id": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", +| "time_stamp": datetime.now(timezone.utc).strftime(DATE_FORMAT), +| "caller_request_id": "0Xe876987694328763492876348928736497869273649" +|} +| +|token = jwt.encode(payload, 'your-at-least-256-bit-secret-token', algorithm='HS256').decode("utf-8") +|authorization = 'DAuth token="{}"'.format(token) +|headers = {'Authorization': authorization} +|url = obp_api_host + '/obp/v4.0.0/users/current' +|req = requests.get(url, headers=headers) +|print(req.text) +|``` +| +|### Under the hood +| +|The file, dauth.scala handles the DAuth, +| +|We: +| +|``` +|-> Check if Props allow_dauth is true +| -> Check if DAuth header exists +| -> Check if getRemoteIpAddress is OK +| -> Look for "token" +| -> parse the JWT token and getOrCreate the user +| -> get the data of the user +|``` +| +|### More information +| +| Parameter names and values are case sensitive. +| Each parameter MUST NOT appear more than once per request. +| + """) + + glossaryItems += GlossaryItem( title = "SCA (Strong Customer Authentication)", From 6131ded99a173146b6c83f8cef89de5c9ed9891d Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 4 Nov 2021 14:02:10 +0100 Subject: [PATCH 108/185] feature/fixed the failed tests --- obp-api/src/test/scala/code/api/dauthTest.scala | 7 +------ obp-api/src/test/scala/code/setup/ServerSetup.scala | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/obp-api/src/test/scala/code/api/dauthTest.scala b/obp-api/src/test/scala/code/api/dauthTest.scala index cacd5d30b..3d990fa29 100644 --- a/obp-api/src/test/scala/code/api/dauthTest.scala +++ b/obp-api/src/test/scala/code/api/dauthTest.scala @@ -6,11 +6,6 @@ import code.setup.{DefaultUsers, PropsReset, ServerSetup} import org.scalatest._ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with PropsReset{ - - - setPropsValues("allow_dauth" -> "true") - setPropsValues("dauth.host" -> "127.0.0.1") - setPropsValues("jwt.token_secret"->"secretsecretsecretstsecretssssss") val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") /* Payload data. verified by wrong secret "123" -- show : DAuthJwtTokenIsNotValid @@ -48,7 +43,7 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P def dauthRequest = baseRequest / "obp" / "v2.0.0" / "users" /"current" <@ (user1) def dauthNonBlockingRequest = baseRequest / "obp" / "v3.0.0" / "users" / "current" <@ (user1) - feature("DAuth in a BLOCKING way") { + feature("DAuth Testing") { scenario("Missing parameter token in a blocking way") { When("We try to login without parameter token in a Header") diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 4dcff76d5..55b45f2a9 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -45,6 +45,9 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("migration_scripts.execute_all" -> "true") setPropsValues("migration_scripts.execute" -> "true") + setPropsValues("allow_dauth" -> "true") + setPropsValues("dauth.host" -> "127.0.0.1") + setPropsValues("jwt.token_secret"->"secretsecretsecretstsecretssssss") val server = TestServer def baseRequest = host(server.host, server.port) From 697c6aef947397fc0ca39ab3d287175ca42b47bc Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 5 Nov 2021 15:41:55 +0100 Subject: [PATCH 109/185] feature/tweaked the DAuth.PayloadOfJwtJSON class --- .../main/scala/code/api/OBPRestHelper.scala | 2 +- obp-api/src/main/scala/code/api/dauth.scala | 51 ++++++------------- .../main/scala/code/api/util/APIUtil.scala | 2 +- .../scala/code/api/util/ErrorMessages.scala | 4 +- .../main/scala/code/api/util/Glossary.scala | 8 +-- .../src/test/scala/code/api/dauthTest.scala | 22 ++++---- .../test/scala/code/setup/ServerSetup.scala | 2 +- 7 files changed, 33 insertions(+), 58 deletions(-) diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 29900da1f..5e169c317 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -416,7 +416,7 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { val s = S DAuth.getOrCreateResourceUser(payload: String, Some(cc)) match { case Full((u, callContext)) => // Authentication is successful - val consumer = DAuth.getOrCreateConsumer(payload, u) + val consumer = DAuth.getConsumerByConsumerKey(payload)//TODO, need to verify the key later. val jwt = DAuth.createJwt(payload) val callContextUpdated = ApiSession.updateCallContext(DAuthResponseHeader(Some(jwt)), callContext) fn(callContextUpdated.map( callContext =>callContext.copy(user = Full(u), consumer = consumer)).getOrElse(callContext.getOrElse(cc).copy(user = Full(u), consumer = consumer))) diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala index a8e98ae81..6ec0efd00 100755 --- a/obp-api/src/main/scala/code/api/dauth.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -33,13 +33,11 @@ import code.model.{Consumer, UserX} import code.users.Users import code.util.Helper.MdcLoggable import com.nimbusds.jwt.JWTClaimsSet -import com.openbankproject.commons.model.{User} +import com.openbankproject.commons.model.User import net.liftweb.common._ import net.liftweb.http._ import net.liftweb.http.rest.RestHelper import net.liftweb.json._ -import net.liftweb.util.Helpers - import com.openbankproject.commons.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -54,10 +52,10 @@ object JSONFactoryDAuth { case class PayloadOfJwtJSON( smart_contract_address: String, network_name: String, - msg_sender: String, - consumer_id: String, - time_stamp: String, - caller_request_id: String + consumer_key: String, + timestamp: Option[String], + msg_sender: Option[String], + request_id: Option[String] ) } @@ -69,17 +67,17 @@ object DAuth extends RestHelper with MdcLoggable { val smartContractAddress = getFieldFromPayloadJson(payloadAsJsonString, "smart_contract_address") val networkName = getFieldFromPayloadJson(payloadAsJsonString, "network_name") val msgSender = getFieldFromPayloadJson(payloadAsJsonString, "msg_sender") - val consumerId = getFieldFromPayloadJson(payloadAsJsonString, "consumer_id") - val timeStamp = getFieldFromPayloadJson(payloadAsJsonString, "time_stamp") - val callerRequestId = getFieldFromPayloadJson(payloadAsJsonString, "caller_request_id") + val consumerKey = getFieldFromPayloadJson(payloadAsJsonString, "consumer_key") + val timeStamp = getFieldFromPayloadJson(payloadAsJsonString, "timestamp") + val requestId = getFieldFromPayloadJson(payloadAsJsonString, "request_id") val json = JSONFactoryDAuth.PayloadOfJwtJSON( smart_contract_address = smartContractAddress, network_name = networkName, - msg_sender = msgSender, - consumer_id = consumerId, - time_stamp = timeStamp, - caller_request_id = callerRequestId + consumer_key = consumerKey, + msg_sender = Some(msgSender), + timestamp = Some(timeStamp), + request_id = Some(requestId) ) val jwtPayloadAsJson = compactRender(Extraction.decompose(json)) val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) @@ -210,28 +208,9 @@ object DAuth extends RestHelper with MdcLoggable { } } - def getOrCreateConsumer(jwtPayload: String, u: User) : Box[Consumer] = { - val consumerId = getFieldFromPayloadJson(jwtPayload, "consumer_id") - val consumerName = getFieldFromPayloadJson(jwtPayload, "msg_sender") - val DAuthValue = "DAuth" // This value is used for Consumer.description - logger.debug("app_id: " + consumerId) - logger.debug("app_name: " + consumerName) - Consumers.consumers.vend.getOrCreateConsumer( - consumerId=Some(consumerId), - Some(Helpers.randomString(40).toLowerCase), - Some(Helpers.randomString(40).toLowerCase), - None, - None, - None, - None, - Some(true), - name = Some(consumerName), - appType = None, - description = Some(DAuthValue), - developerEmail = None, - redirectURL = None, - createdByUserId = Some(u.userId) - ) + def getConsumerByConsumerKey(jwtPayload: String) : Box[Consumer] = { + val consumeyKey = getFieldFromPayloadJson(jwtPayload, "consumer_key") + Consumers.consumers.vend.getConsumerByConsumerKey(consumeyKey) } // Return a Map containing the DAuth parameter : token -> value diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 05daae483..025a128ab 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2748,7 +2748,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case Full(payload) => DAuth.getOrCreateResourceUserFuture(payload: String, Some(cc)) map { case Full((u,callContext)) => // Authentication is successful - val consumer = DAuth.getOrCreateConsumer(payload, u) + val consumer = DAuth.getConsumerByConsumerKey(payload) val jwt = DAuth.createJwt(payload) val callContextUpdated = ApiSession.updateCallContext(DAuthResponseHeader(Some(jwt)), callContext) (Full(u), callContextUpdated.map(_.copy(consumer=consumer, user = Full(u)))) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 6bc843ad5..eb645a3ad 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -154,7 +154,7 @@ object ErrorMessages { val GatewayLoginUnknownError = "OBP-20029: Unknown Gateway login error." val GatewayLoginHostPropertyMissing = "OBP-20030: Property gateway.host is not defined." val GatewayLoginWhiteListAddresses = "OBP-20031: Gateway login can be done only from allowed addresses." - val GatewayLoginJwtTokenIsNotValid = "OBP-20040: The JWT is corrupted/changed during a transport." + val GatewayLoginJwtTokenIsNotValid = "OBP-20040: The Gateway login JWT is corrupted/changed during a transport." val GatewayLoginCannotExtractJwtToken = "OBP-20041: Header, Payload and Signature cannot be extracted from the JWT." val GatewayLoginNoNeedToCallCbs = "OBP-20042: There is no need to call CBS" val GatewayLoginCannotFindUser = "OBP-20043: User cannot be found. Please initiate CBS communication in order to create it." @@ -188,7 +188,7 @@ object ErrorMessages { val DAuthHostPropertyMissing = "OBP-20068: Property dauth.host is not defined." val DAuthWhiteListAddresses = "OBP-20069: DAuth login can be done only from allowed addresses." val DAuthNoJwtForResponse = "OBP-20070: There is no useful value for JWT." - val DAuthJwtTokenIsNotValid = "OBP-20071: The JWT is corrupted/changed during a transport." + val DAuthJwtTokenIsNotValid = "OBP-20071: The DAuth JWT is corrupted/changed during a transport." val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " 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 a3663dd02..972ab469c 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2075,8 +2075,8 @@ object Glossary extends MdcLoggable { | "network_name": "ETHEREUM", | "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", | "consumer_id": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", -| "time_stamp": "2021-11-04T14:13:40Z", -| "caller_request_id": "0Xe876987694328763492876348928736497869273649" +| "timestamp": "2021-11-04T14:13:40Z", +| "request_id": "0Xe876987694328763492876348928736497869273649" |} |``` |VERIFY SIGNATURE @@ -2159,8 +2159,8 @@ object Glossary extends MdcLoggable { | "network_name": "ETHEREUM", | "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", | "consumer_id": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", -| "time_stamp": datetime.now(timezone.utc).strftime(DATE_FORMAT), -| "caller_request_id": "0Xe876987694328763492876348928736497869273649" +| "timestamp": datetime.now(timezone.utc).strftime(DATE_FORMAT), +| "request_id": "0Xe876987694328763492876348928736497869273649" |} | |token = jwt.encode(payload, 'your-at-least-256-bit-secret-token', algorithm='HS256').decode("utf-8") diff --git a/obp-api/src/test/scala/code/api/dauthTest.scala b/obp-api/src/test/scala/code/api/dauthTest.scala index 3d990fa29..9e79c0c2f 100644 --- a/obp-api/src/test/scala/code/api/dauthTest.scala +++ b/obp-api/src/test/scala/code/api/dauthTest.scala @@ -13,28 +13,24 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P "smart_contract_address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "network_name": "ETHEREUM", "msg_sender": "0xe90980927f1725E7734CE288F8367e1Bb143E90fhku767", - "consumer_id": "0x19255a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", - "time_stamp": "2018-08-20T14:13:40Z", - "caller_request_id": "0Xe876987694328763492876348928736497869273649" + "consumer_key": "0x19255a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", + "timestamp": "2018-08-20T14:13:40Z", + "request_id": "0Xe876987694328763492876348928736497869273649" } */ - val invalidSecretJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfaWQiOiIweDE5MjU1YTRlYzMxZTg5Y2VhNTRkMWYxMjVkYjc1MzZlODc0YWI0YTk2YjRkNGY2NDM4NjY4YjZiYjEwYTZhZGIiLCJ0aW1lX3N0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJjYWxsZXJfcmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.5t1bolx13gCBSyvbTzv_QWP1tFkN0m_Sv727bB1QZuw" + val invalidSecretJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfaWQiOiIweDE5MjU1YTRlYzMxZTg5Y2VhNTRkMWYxMjVkYjc1MzZlODc0YWI0YTk2YjRkNGY2NDM4NjY4YjZiYjEwYTZhZGIiLCJ0aW1lc3RhbXAiOiIyMDE4LTA4LTIwVDE0OjEzOjQwWiIsInJlcXVlc3RfaWQiOiIwWGU4NzY5ODc2OTQzMjg3NjM0OTI4NzYzNDg5Mjg3MzY0OTc4NjkyNzM2NDkifQ.qWI4DXwa8QDGVPJoPCJehkLKHFA2A4_77JHINluc2tc" - /* Payload data. verified by correct secret "secretsecretsecretstsecretssssss" + /* Payload data. verified by correct secret "your-at-least-256-bit-secret-token" { "smart_contract_address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F05124", "network_name": "ETHEREUM", "msg_sender": "0xe90980927f1725E7734CE288F8367e1Bb143E90fhku767", - "consumer_id": "0x19255a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", - "time_stamp": "2018-08-20T14:13:40Z", - "caller_request_id": "0Xe876987694328763492876348928736497869273649" + "consumer_key": "0x19255a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", + "timestamp": "2018-08-20T14:13:40Z", + "request_id": "0Xe876987694328763492876348928736497869273649" } */ - val jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3Z" + - "TFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhG" + - "ODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfaWQiOiIweDE5MjU1YTRlYzMxZTg5Y2VhNTRkMWYxMjVkYjc1MzZlODc0YWI0YTk2YjRkNGY2" + - "NDM4NjY4YjZiYjEwYTZhZGIiLCJ0aW1lX3N0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJjYWxsZXJfcmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5" + - "NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.us2wjYUwiQHYdmU9JBNEMz8rc8qVGzY6bDNeknC3HMo" + val jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyNCIsIm5ldHdvcmtfbmFtZSI6IkVUSEVSRVVNIiwibXNnX3NlbmRlciI6IjB4ZTkwOTgwOTI3ZjE3MjVFNzczNENFMjg4RjgzNjdlMUJiMTQzRTkwZmhrdTc2NyIsImNvbnN1bWVyX2tleSI6IjB4MTkyNTVhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMTgtMDgtMjBUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.Wg2BYYsbWK-MUYToeTUc0GvDwcwnkR6Dh4SV-pMjChk" val invalidJwt = ("Authorization", ("DAuth token=%s").format(invalidSecretJwt)) val validJwt = ("Authorization", ("DAuth token=%s").format(jwt)) diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 55b45f2a9..8a26eeefe 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -47,7 +47,7 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("migration_scripts.execute" -> "true") setPropsValues("allow_dauth" -> "true") setPropsValues("dauth.host" -> "127.0.0.1") - setPropsValues("jwt.token_secret"->"secretsecretsecretstsecretssssss") + setPropsValues("jwt.token_secret"->"your-at-least-256-bit-secret-token") val server = TestServer def baseRequest = host(server.host, server.port) From b06077b9c314358306fc17e87293286c671c4bfb Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 5 Nov 2021 17:06:39 +0100 Subject: [PATCH 110/185] feature/tweaked the header format, now support DAuth:xxx --- .../main/scala/code/api/OBPRestHelper.scala | 17 ++- obp-api/src/main/scala/code/api/dauth.scala | 115 +++--------------- .../main/scala/code/api/util/APIUtil.scala | 21 ++-- .../main/scala/code/api/util/ApiSession.scala | 2 +- .../scala/code/api/util/ErrorMessages.scala | 1 + 5 files changed, 40 insertions(+), 116 deletions(-) diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 5e169c317..8456b21f8 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -28,12 +28,11 @@ TESOBE (http://www.tesobe.com/) package code.api import java.net.URLDecoder - import code.api.Constant._ import code.api.OAuthHandshake._ import code.api.builder.AccountInformationServiceAISApi.APIMethods_AccountInformationServiceAISApi import code.api.util.APIUtil._ -import code.api.util.ErrorMessages.{UserIsDeleted, UsernameHasBeenLocked, attemptedToOpenAnEmptyBox} +import code.api.util.ErrorMessages.{InvalidDAuthHeaderToken, UserIsDeleted, UsernameHasBeenLocked, attemptedToOpenAnEmptyBox} import code.api.util._ import code.api.v3_0_0.APIMethods300 import code.api.v3_1_0.APIMethods310 @@ -402,15 +401,15 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { Failure(ErrorMessages.GatewayLoginUnknownError) } } - else if (APIUtil.getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(authorization)) { + else if (APIUtil.getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.requestHeaders)) { logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature val s = S - val (httpCode, message, parameters) = DAuth.validator(s.request) - httpCode match { - case 200 => - val payload = DAuth.parseJwt(parameters) + val dauthToken = DAuth.getDAuthToken(cc.requestHeaders) + dauthToken match { + case Some(token :: _) => + val payload = DAuth.parseJwt(token) payload match { case Full(payload) => val s = S @@ -421,7 +420,7 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { val callContextUpdated = ApiSession.updateCallContext(DAuthResponseHeader(Some(jwt)), callContext) fn(callContextUpdated.map( callContext =>callContext.copy(user = Full(u), consumer = consumer)).getOrElse(callContext.getOrElse(cc).copy(user = Full(u), consumer = consumer))) case Failure(msg, t, c) => Failure(msg, t, c) - case _ => Full(errorJsonResponse(payload, httpCode)) + case _ => Full(errorJsonResponse(payload)) } case Failure(msg, t, c) => Failure(msg, t, c) @@ -429,7 +428,7 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { Failure(ErrorMessages.DAuthUnknownError) } case _ => - Failure(message) + Failure(InvalidDAuthHeaderToken) } case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == false) => // All other addresses will be rejected Failure(ErrorMessages.DAuthWhiteListAddresses) diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala index 6ec0efd00..f6f29b9f0 100755 --- a/obp-api/src/main/scala/code/api/dauth.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -39,6 +39,9 @@ import net.liftweb.http._ import net.liftweb.http.rest.RestHelper import net.liftweb.json._ import com.openbankproject.commons.ExecutionContext.Implicits.global +import net.liftweb.http.provider.HTTPParam + +import scala.collection.immutable.List import scala.concurrent.Future /** @@ -90,9 +93,8 @@ object DAuth extends RestHelper with MdcLoggable { } } - def parseJwt(parameters: Map[String, String]): Box[String] = { - val jwt = getToken(parameters) - logger.debug("parseJwt says jwt.toString is: " + jwt.toString) + def parseJwt(jwt:String): Box[String] = { + logger.debug("parseJwt says jwt.toString is: " + jwt) logger.debug("parseJwt says: validateJwtToken(jwt) is:" + validateJwtToken(jwt)) validateJwtToken(jwt) match { case Full(jwtPayload) => @@ -127,32 +129,8 @@ object DAuth extends RestHelper with MdcLoggable { } // Check if the request (access token or request token) is valid and return a tuple - def validator(request: Box[Req]) : (Int, String, Map[String,String]) = { - // First we try to extract all parameters from a Request - val parameters: Map[String, String] = getAllParameters(request) - val emptyMap = Map[String, String]() - - parameters.get("error") match { - case Some(m) => { - logger.error("DAuth error message : " + m) - (400, m, emptyMap) - } - case _ => { - // Are all the necessary DAuth parameters present? - val missingParams: Set[String] = missingDAuthParameters(parameters) - missingParams.nonEmpty match { - case true => { - val message = ErrorMessages.DAuthMissingParameters + missingParams.mkString(", ") - logger.error("DAuth error message : " + message) - (400, message, emptyMap) - } - case false => { - logger.debug("DAuth parameters : " + parameters) - (200, "", parameters) - } - } - } - } + def getDAuthToken(requestHeaders: List[HTTPParam]) : Option[List[String]] = { + requestHeaders.find(_.name=="DAuth").map(_.values) } def getOrCreateResourceUser(jwtPayload: String, callContext: Option[CallContext]) : Box[(User, Option[CallContext])] = { @@ -213,60 +191,6 @@ object DAuth extends RestHelper with MdcLoggable { Consumers.consumers.vend.getConsumerByConsumerKey(consumeyKey) } - // Return a Map containing the DAuth parameter : token -> value - def getAllParameters(request: Box[Req]): Map[String, String] = { - def toMap(parametersList: String) = { - //transform the string "DAuth token="value"" - //to a tuple (DAuth_parameter,Decoded(value)) - def dynamicListExtract(input: String) = { - val DAuthPossibleParameters = - List( - "token" - ) - if (input contains "=") { - val split = input.split("=", 2) - val parameterValue = split(1).replace("\"", "") - //add only OAuth parameters and not empty - if (DAuthPossibleParameters.contains(split(0)) && !parameterValue.isEmpty) - Some(split(0), parameterValue) // return key , value - else - None - } - else - None - } - // We delete the "DAuth" prefix and all the white spaces that may exist in the string - val cleanedParameterList = parametersList.stripPrefix("DAuth").replaceAll("\\s", "") - val params = Map(cleanedParameterList.split(",").flatMap(dynamicListExtract _): _*) - params - } - - request match { - case Full(a) => a.header("Authorization") match { - case Full(header) => { - if (header.contains("DAuth")) - toMap(header) - else - Map("error" -> "Missing DAuth in header!") - } - case _ => Map("error" -> "Missing Authorization header!") - } - case _ => Map("error" -> "Request is incorrect!") - } - } - - // Returns the missing parameters - def missingDAuthParameters(parameters: Map[String, String]): Set[String] = { - ("token" :: List()).toSet diff parameters.keySet - } - - private def getToken(params: Map[String, String]): String = { - logger.debug("getToken params are: " + params.toString()) - val token = params.getOrElse("token", "") - logger.debug("getToken wants to return token: " + token) - token - } - private def getFieldFromPayloadJson(payloadAsJsonString: String, fieldName: String) = { val jwtJson = parse(payloadAsJsonString) // Transform Json string to JsonAST val v = jwtJson.\(fieldName) @@ -277,7 +201,6 @@ object DAuth extends RestHelper with MdcLoggable { compactRender(v).replace("\"", "") } } - // Try to find errorCode in Json string received from South side and extract to list // Return list of error codes values def getErrors(message: String) : List[String] = { @@ -293,22 +216,16 @@ object DAuth extends RestHelper with MdcLoggable { listOfValues } - def getUser : Box[User] = { - val (httpCode, message, parameters) = DAuth.validator(S.request) - httpCode match { - case 200 => - val payload = DAuth.parseJwt(parameters) - payload match { - case Full(payload) => - val username = getFieldFromPayloadJson(payload, "smart_contract_address") - val provider = getFieldFromPayloadJson(payload, "network_name") - logger.debug("username: " + username) - Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = username) - case _ => - None - } - case _ => + val token = S.getRequestHeader("DAuth") + val payload = token.map(DAuth.parseJwt).flatten + payload match { + case Full(payload) => + val username = getFieldFromPayloadJson(payload, "smart_contract_address") + val provider = getFieldFromPayloadJson(payload, "network_name") + logger.debug("username: " + username) + Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = username) + case _ => None } } diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 025a128ab..bda5055a1 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -181,7 +181,14 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ def hasGatewayHeader(authorization: Box[String]) = hasHeader("GatewayLogin", authorization) - def hasDAuthHeader(authorization: Box[String]) = hasHeader("DAuth", authorization) + /** + * The value `DAuth` is in the KEY + * DAuth:xxxxx + * + * Other types: the `GatewayLogin` is in the VALUE + * Authorization:GatewayLogin token=xxxx + */ + def hasDAuthHeader(requestHeaders: List[HTTPParam]) = requestHeaders.map(_.name).exists(_ =="DAuth") /** * Helper function which tells us does an "Authorization" request header field has the Type of an authentication scheme @@ -2736,14 +2743,14 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ Future { (Failure(ErrorMessages.GatewayLoginUnknownError), None) } } } // DAuth Login - else if (getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.authReqHeaderField)) { + else if (getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.requestHeaders)) { logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature - val (httpCode, message, parameters) = DAuth.validator(s.request) - httpCode match { - case 200 => - val payload = DAuth.parseJwt(parameters) + val dauthToken = DAuth.getDAuthToken(cc.requestHeaders) + dauthToken match { + case Some(token :: _) => + val payload = DAuth.parseJwt(token) payload match { case Full(payload) => DAuth.getOrCreateResourceUserFuture(payload: String, Some(cc)) map { @@ -2763,7 +2770,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ Future { (Failure(ErrorMessages.DAuthUnknownError), None) } } case _ => - Future { (Failure(message), None) } + Future { (Failure(InvalidDAuthHeaderToken), None) } } case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == false) => // All other addresses will be rejected Future { (Failure(ErrorMessages.DAuthWhiteListAddresses), None) } diff --git a/obp-api/src/main/scala/code/api/util/ApiSession.scala b/obp-api/src/main/scala/code/api/util/ApiSession.scala index 889397845..d6bbce143 100644 --- a/obp-api/src/main/scala/code/api/util/ApiSession.scala +++ b/obp-api/src/main/scala/code/api/util/ApiSession.scala @@ -140,7 +140,7 @@ case class CallContext( def authType: AuthenticationType = { if(hasGatewayHeader(authReqHeaderField)) { GatewayLogin - } else if(hasDAuthHeader(authReqHeaderField)) { // DAuth Login + } else if(requestHeaders.exists(_.name=="DAuth")) { // DAuth Login DAuth } else if(has2021DirectLoginHeader(requestHeaders)) { // Direct Login DirectLogin diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index eb645a3ad..50a057f4b 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -189,6 +189,7 @@ object ErrorMessages { val DAuthWhiteListAddresses = "OBP-20069: DAuth login can be done only from allowed addresses." val DAuthNoJwtForResponse = "OBP-20070: There is no useful value for JWT." val DAuthJwtTokenIsNotValid = "OBP-20071: The DAuth JWT is corrupted/changed during a transport." + val InvalidDAuthHeaderToken = "OBP-20072: DAuth Header value should be one single string." val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " From ce482c132922b55cefde74967b481ae34e2706f5 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 8 Nov 2021 12:50:08 +0100 Subject: [PATCH 111/185] feature/tweaked the payload case class and the header format --- .../main/scala/code/api/OBPRestHelper.scala | 6 ++-- obp-api/src/main/scala/code/api/dauth.scala | 4 +-- .../main/scala/code/api/util/APIUtil.scala | 32 +++---------------- .../main/scala/code/api/util/ApiSession.scala | 2 +- .../main/scala/code/api/util/Glossary.scala | 27 +++++++--------- .../test/scala/code/setup/ServerSetup.scala | 2 +- 6 files changed, 23 insertions(+), 50 deletions(-) diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 8456b21f8..4ec40f92e 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -401,18 +401,16 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { Failure(ErrorMessages.GatewayLoginUnknownError) } } - else if (APIUtil.getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.requestHeaders)) { - logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) + else if (APIUtil.getPropsAsBoolValue("allow_dauth_login", false) && hasDAuthHeader(cc.requestHeaders)) { + logger.info("allow_dauth_login-getRemoteIpAddress: " + remoteIpAddress ) APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature - val s = S val dauthToken = DAuth.getDAuthToken(cc.requestHeaders) dauthToken match { case Some(token :: _) => val payload = DAuth.parseJwt(token) payload match { case Full(payload) => - val s = S DAuth.getOrCreateResourceUser(payload: String, Some(cc)) match { case Full((u, callContext)) => // Authentication is successful val consumer = DAuth.getConsumerByConsumerKey(payload)//TODO, need to verify the key later. diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala index f6f29b9f0..4835b798a 100755 --- a/obp-api/src/main/scala/code/api/dauth.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -130,7 +130,7 @@ object DAuth extends RestHelper with MdcLoggable { // Check if the request (access token or request token) is valid and return a tuple def getDAuthToken(requestHeaders: List[HTTPParam]) : Option[List[String]] = { - requestHeaders.find(_.name=="DAuth").map(_.values) + requestHeaders.find(_.name==APIUtil.DAuthHeaderKey).map(_.values) } def getOrCreateResourceUser(jwtPayload: String, callContext: Option[CallContext]) : Box[(User, Option[CallContext])] = { @@ -217,7 +217,7 @@ object DAuth extends RestHelper with MdcLoggable { } def getUser : Box[User] = { - val token = S.getRequestHeader("DAuth") + val token = S.getRequestHeader(APIUtil.DAuthHeaderKey) val payload = token.map(DAuth.parseJwt).flatten payload match { case Full(payload) => diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index bda5055a1..d9eea8893 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -188,7 +188,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ * Other types: the `GatewayLogin` is in the VALUE * Authorization:GatewayLogin token=xxxx */ - def hasDAuthHeader(requestHeaders: List[HTTPParam]) = requestHeaders.map(_.name).exists(_ =="DAuth") + def hasDAuthHeader(requestHeaders: List[HTTPParam]) = requestHeaders.map(_.name).exists(_ ==DAuthHeaderKey) /** * Helper function which tells us does an "Authorization" request header field has the Type of an authentication scheme @@ -2256,29 +2256,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ /** * Defines DAuth Custom Response Header. */ - val DAuthResponseHeaderName = "DAuth" - /** - * Set value of DAuth Custom Response Header. - */ - def setDAuthResponseHeader(s: S)(value: String) = s.setSessionAttribute(DAuthResponseHeaderName, value) - /** - * @return - DAuth Custom Response Header. - */ - def getDAuthResponseHeader() = { - S.getSessionAttribute(DAuthResponseHeaderName) match { - case Full(h) => List((DAuthResponseHeaderName, h)) - case _ => Nil - } - } - def getDAuthJwt(): Option[String] = { - getDAuthResponseHeader() match { - case x :: Nil => - Some(x._2) - case _ => - None - } - } - + val DAuthHeaderKey = "DAuth" /** * Turn a string of format "FooBar" into snake case "foo_bar" * @@ -2743,8 +2721,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ Future { (Failure(ErrorMessages.GatewayLoginUnknownError), None) } } } // DAuth Login - else if (getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.requestHeaders)) { - logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) + else if (getPropsAsBoolValue("allow_dauth_login", false) && hasDAuthHeader(cc.requestHeaders)) { + logger.info("allow_dauth_login-getRemoteIpAddress: " + remoteIpAddress ) APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature val dauthToken = DAuth.getDAuthToken(cc.requestHeaders) @@ -2755,7 +2733,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case Full(payload) => DAuth.getOrCreateResourceUserFuture(payload: String, Some(cc)) map { case Full((u,callContext)) => // Authentication is successful - val consumer = DAuth.getConsumerByConsumerKey(payload) + val consumer = DAuth.getConsumerByConsumerKey(payload)//TODO, need to verify the key later. val jwt = DAuth.createJwt(payload) val callContextUpdated = ApiSession.updateCallContext(DAuthResponseHeader(Some(jwt)), callContext) (Full(u), callContextUpdated.map(_.copy(consumer=consumer, user = Full(u)))) diff --git a/obp-api/src/main/scala/code/api/util/ApiSession.scala b/obp-api/src/main/scala/code/api/util/ApiSession.scala index d6bbce143..8f2607e77 100644 --- a/obp-api/src/main/scala/code/api/util/ApiSession.scala +++ b/obp-api/src/main/scala/code/api/util/ApiSession.scala @@ -140,7 +140,7 @@ case class CallContext( def authType: AuthenticationType = { if(hasGatewayHeader(authReqHeaderField)) { GatewayLogin - } else if(requestHeaders.exists(_.name=="DAuth")) { // DAuth Login + } else if(requestHeaders.exists(_.name==DAuthHeaderKey)) { // DAuth Login DAuth } else if(has2021DirectLoginHeader(requestHeaders)) { // Direct Login DirectLogin 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 972ab469c..4699d751b 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2016,12 +2016,12 @@ object Glossary extends MdcLoggable { """) - val dauthEnabledMessage : String = if (APIUtil.getPropsAsBoolValue("allow_gateway_login", false)) + val dauthEnabledMessage : String = if (APIUtil.getPropsAsBoolValue("allow_dauth_login", false)) {"Note: DAuth is enabled."} else {"Note: *DAuth is NOT enabled on this instance!*"} glossaryItems += GlossaryItem( - title = "DAuth", + title = APIUtil.DAuthHeaderKey, description = s""" |### Introduction @@ -2032,7 +2032,7 @@ object Glossary extends MdcLoggable { | |Note: DAuth does *not* require an explicit POST like Direct Login to create the token. | -|The **Gateway is responsible** for creating a token which is trusted by OBP **absolutely**! +|The **DAuth is responsible** for creating a token which is trusted by OBP **absolutely**! | |When OBP receives a token via DAuth, OBP creates or gets a user based on the username (smart_contract_address) supplied. | @@ -2048,7 +2048,7 @@ object Glossary extends MdcLoggable { |# jwt.token_secret=your-at-least-256-bit-secret-token |# Enable/Disable DAuth communication at all |# In case isn't defined default value is false -|# allow_dauth=false +|# allow_dauth_login=false |# Define comma separated list of allowed IP addresses |# dauth.host=127.0.0.1 |# -------------------------------------- DAuth-- @@ -2074,7 +2074,7 @@ object Glossary extends MdcLoggable { | "smart_contract_address": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", | "network_name": "ETHEREUM", | "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", -| "consumer_id": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", +| "consumer_key": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", | "timestamp": "2021-11-04T14:13:40Z", | "request_id": "0Xe876987694328763492876348928736497869273649" |} @@ -2091,9 +2091,7 @@ object Glossary extends MdcLoggable { |Here is the above example token: | |``` -|eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. -|eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9pZCI6IjB4MTIzNGE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZV9zdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwiY2FsbGVyX3JlcXVlc3RfaWQiOiIwWGU4NzY5ODc2OTQzMjg3NjM0OTI4NzYzNDg5Mjg3MzY0OTc4NjkyNzM2NDkifQ. -|SbgXzyRNd6uLBYql_fwXi3KAWS8SaKYMHmnVFgbGRiY +|eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9rZXkiOiIweDEyMzRhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.XSiQxjEVyCouf7zT8MubEKsbOBZuReGVhnt9uck6z6k |``` | | @@ -2120,12 +2118,12 @@ object Glossary extends MdcLoggable { | Host: localhost:8080 | User-Agent: curl/7.47.0 | Accept: */* -| Authorization: GatewayLogin token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9pZCI6IjB4MTIzNGE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZV9zdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwiY2FsbGVyX3JlcXVlc3RfaWQiOiIwWGU4NzY5ODc2OTQzMjg3NjM0OTI4NzYzNDg5Mjg3MzY0OTc4NjkyNzM2NDkifQ.SbgXzyRNd6uLBYql_fwXi3KAWS8SaKYMHmnVFgbGRiY" +| DAuth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9rZXkiOiIweDEyMzRhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.XSiQxjEVyCouf7zT8MubEKsbOBZuReGVhnt9uck6z6k | |CURL example | |``` -|curl -v -H 'Authorization: DAuth token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9pZCI6IjB4MTIzNGE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZV9zdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwiY2FsbGVyX3JlcXVlc3RfaWQiOiIwWGU4NzY5ODc2OTQzMjg3NjM0OTI4NzYzNDg5Mjg3MzY0OTc4NjkyNzM2NDkifQ.SbgXzyRNd6uLBYql_fwXi3KAWS8SaKYMHmnVFgbGRiY"' $getServerUrl/obp/v3.0.0/users/current +|curl -v -H 'DAuth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9rZXkiOiIweDEyMzRhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.XSiQxjEVyCouf7zT8MubEKsbOBZuReGVhnt9uck6z6k' $getServerUrl/obp/v3.0.0/users/current |``` | | @@ -2157,15 +2155,14 @@ object Glossary extends MdcLoggable { |payload = { | "smart_contract_address": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", | "network_name": "ETHEREUM", -| "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", -| "consumer_id": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", +| "consumer_key": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", | "timestamp": datetime.now(timezone.utc).strftime(DATE_FORMAT), +| "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", | "request_id": "0Xe876987694328763492876348928736497869273649" |} | |token = jwt.encode(payload, 'your-at-least-256-bit-secret-token', algorithm='HS256').decode("utf-8") -|authorization = 'DAuth token="{}"'.format(token) -|headers = {'Authorization': authorization} +|headers = {'DAuth': token} |url = obp_api_host + '/obp/v4.0.0/users/current' |req = requests.get(url, headers=headers) |print(req.text) @@ -2178,7 +2175,7 @@ object Glossary extends MdcLoggable { |We: | |``` -|-> Check if Props allow_dauth is true +|-> Check if Props allow_dauth_login is true | -> Check if DAuth header exists | -> Check if getRemoteIpAddress is OK | -> Look for "token" diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 8a26eeefe..ec0546e8b 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -45,7 +45,7 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("migration_scripts.execute_all" -> "true") setPropsValues("migration_scripts.execute" -> "true") - setPropsValues("allow_dauth" -> "true") + setPropsValues("allow_dauth_login" -> "true") setPropsValues("dauth.host" -> "127.0.0.1") setPropsValues("jwt.token_secret"->"your-at-least-256-bit-secret-token") From ac45a044b29324d25113a70ccef31f3d954b8623 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 8 Nov 2021 12:52:24 +0100 Subject: [PATCH 112/185] feature/rename rest_connector_http_header_signature -> rest_connector_sends_x-sign_header --- obp-api/src/main/resources/props/sample.props.template | 6 +++--- .../code/bankconnectors/rest/RestConnector_vMar2019.scala | 2 +- release_notes.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 074f4f42f..dcf17b0f9 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -816,9 +816,9 @@ featured_apis=elasticSearchWarehouseV300 # If Rest Connector do not get the response in the following seconds, it will throw the error message back. # This props can be omitted, the default value is 59. It should be less than Nginx timeout. # rest2019_connector_timeout = 59 -# If set it to `true`, it will add the signature (SHA256WithRSA) into each the rest connector http calls, and you need to upload the RSA -# private key into the UserAuthContext. -#rest_connector_http_header_signature=false +# If set it to `true`, it will add the x-sign (SHA256WithRSA) into each the rest connector http calls, +# and you need to upload the RSA private key into the UserAuthContext. +#rest_connector_sends_x-sign_header=false # -- Scopes ----------------------------------------------------- diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index 9411c2a8c..1d9c6dc33 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -6580,7 +6580,7 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable callContext: Option[CallContext] ): List[HttpHeader] = { - val needSignatureHead = APIUtil.getPropsAsBoolValue("rest_connector_http_header_signature", false) + val needSignatureHead = APIUtil.getPropsAsBoolValue("rest_connector_sends_x-sign_header", false) val generalContext = callContext.flatMap(_.toOutboundAdapterCallContext.generalContext).getOrElse(List.empty[BasicGeneralContext]) val headersFromGeneralContext = generalContext.map(generalContext => RawHeader(generalContext.key,generalContext.value)) diff --git a/release_notes.md b/release_notes.md index 612c6f8f8..a8a9ce2dc 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,7 +3,7 @@ ### Most recent changes at top of file ``` Date Commit Action -01/11/2021 03305d2b Added props: rest_connector_http_header_signature, default is false +01/11/2021 03305d2b Added props: rest_connector_sends_x-sign_header, default is false 17/09/2021 e65cd51d Added props: webui_main_faq_external_link, default is obp static file: /main-faq.html 09/09/2021 65952225 Added props: webui_support_email, default is contact@openbankproject.com 02/09/2021 a826d908 Renamed Web UI props: From 4510b0d5c836267b6d22a59f2ca049010df13586 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 8 Nov 2021 13:56:28 +0100 Subject: [PATCH 113/185] feature/fixed the DAuth Test --- .../src/test/scala/code/api/dauthTest.scala | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/obp-api/src/test/scala/code/api/dauthTest.scala b/obp-api/src/test/scala/code/api/dauthTest.scala index 9e79c0c2f..84006f700 100644 --- a/obp-api/src/test/scala/code/api/dauthTest.scala +++ b/obp-api/src/test/scala/code/api/dauthTest.scala @@ -18,7 +18,7 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P "request_id": "0Xe876987694328763492876348928736497869273649" } */ - val invalidSecretJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfaWQiOiIweDE5MjU1YTRlYzMxZTg5Y2VhNTRkMWYxMjVkYjc1MzZlODc0YWI0YTk2YjRkNGY2NDM4NjY4YjZiYjEwYTZhZGIiLCJ0aW1lc3RhbXAiOiIyMDE4LTA4LTIwVDE0OjEzOjQwWiIsInJlcXVlc3RfaWQiOiIwWGU4NzY5ODc2OTQzMjg3NjM0OTI4NzYzNDg5Mjg3MzY0OTc4NjkyNzM2NDkifQ.qWI4DXwa8QDGVPJoPCJehkLKHFA2A4_77JHINluc2tc" + val invalidSecretJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfa2V5IjoiMHgxOTI1NWE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZXN0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJyZXF1ZXN0X2lkIjoiMFhlODc2OTg3Njk0MzI4NzYzNDkyODc2MzQ4OTI4NzM2NDk3ODY5MjczNjQ5In0.mK4Bx-V3reGe2jWxvQ5NQNSLXZ7AVRTX2fUFLD-2sSs" /* Payload data. verified by correct secret "your-at-least-256-bit-secret-token" { @@ -32,24 +32,16 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P */ val jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyNCIsIm5ldHdvcmtfbmFtZSI6IkVUSEVSRVVNIiwibXNnX3NlbmRlciI6IjB4ZTkwOTgwOTI3ZjE3MjVFNzczNENFMjg4RjgzNjdlMUJiMTQzRTkwZmhrdTc2NyIsImNvbnN1bWVyX2tleSI6IjB4MTkyNTVhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMTgtMDgtMjBUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.Wg2BYYsbWK-MUYToeTUc0GvDwcwnkR6Dh4SV-pMjChk" - val invalidJwt = ("Authorization", ("DAuth token=%s").format(invalidSecretJwt)) - val validJwt = ("Authorization", ("DAuth token=%s").format(jwt)) - val missingParameterToken = ("Authorization", ("DAuth wrong_parameter_name=%s").format(jwt)) + val invalidJwt = ("DAuth", ("%s").format(invalidSecretJwt)) + val validJwt = ("DAuth", ("%s").format(jwt)) - def dauthRequest = baseRequest / "obp" / "v2.0.0" / "users" /"current" <@ (user1) - def dauthNonBlockingRequest = baseRequest / "obp" / "v3.0.0" / "users" / "current" <@ (user1) + def dauthRequest = baseRequest / "obp" / "v2.0.0" / "users" /"current" + def dauthNonBlockingRequest = baseRequest / "obp" / "v3.0.0" / "users" / "current" feature("DAuth Testing") { scenario("Missing parameter token in a blocking way") { When("We try to login without parameter token in a Header") - val response = makeGetRequest(dauthRequest, List(missingParameterToken)) - Then("We should get a 400 - Bad Request") - logger.debug("-----------------------------------------") - logger.debug(response) - logger.debug("-----------------------------------------") - response.code should equal(400) - response.toString contains (ErrorMessages.DAuthMissingParameters) should be (true) When("We try to login with an invalid JWT") val responseInvalid = makeGetRequest(dauthRequest, List(invalidJwt)) @@ -67,15 +59,6 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P logger.debug("-----------------------------------------") responseValidJwt.code should equal(200) - When("We try to login without parameter token in a Header") - val responseNonBlocking = makeGetRequest(dauthNonBlockingRequest, List(missingParameterToken)) - Then("We should get a 400 - Bad Request") - logger.debug("-----------------------------------------") - logger.debug("responseNonBlocking: "+ responseNonBlocking) - logger.debug("-----------------------------------------") - responseNonBlocking.code should equal(401) - responseNonBlocking.toString contains (ErrorMessages.DAuthMissingParameters) should be (true) - When("We try to login with an invalid JWT") val responseNonBlockingInvalid = makeGetRequest(dauthNonBlockingRequest, List(invalidJwt)) Then("We should get a 400 - Bad Request") From e6b2354f67be3bc221022fadc30dcf0e210b13d5 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 8 Nov 2021 17:20:11 +0100 Subject: [PATCH 114/185] refactor/tweaked the names for DAuth --- obp-api/src/main/resources/props/sample.props.template | 4 ++-- obp-api/src/main/scala/code/api/OBPRestHelper.scala | 4 ++-- obp-api/src/main/scala/code/api/util/APIUtil.scala | 4 ++-- obp-api/src/main/scala/code/api/util/Glossary.scala | 6 +++--- obp-api/src/test/scala/code/setup/ServerSetup.scala | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index dcf17b0f9..70f3225da 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -678,7 +678,7 @@ autocomplete_at_login_form_enabled=false # control access to customer firehose. # allow_customer_firehose=false -# -- Gateway And DAuth common Setting-------------------------------------- +# -- Gateway Login And DAuth common settings-------------------------------------- # Define secret used to validate JWT token # jwt.token_secret=your-at-least-256-bit-secret-token @@ -817,7 +817,7 @@ featured_apis=elasticSearchWarehouseV300 # This props can be omitted, the default value is 59. It should be less than Nginx timeout. # rest2019_connector_timeout = 59 # If set it to `true`, it will add the x-sign (SHA256WithRSA) into each the rest connector http calls, -# and you need to upload the RSA private key into the UserAuthContext. +# please add the name of the field for the UserAuthContext and/or link to other documentation.. #rest_connector_sends_x-sign_header=false diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index 4ec40f92e..323278334 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -401,8 +401,8 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { Failure(ErrorMessages.GatewayLoginUnknownError) } } - else if (APIUtil.getPropsAsBoolValue("allow_dauth_login", false) && hasDAuthHeader(cc.requestHeaders)) { - logger.info("allow_dauth_login-getRemoteIpAddress: " + remoteIpAddress ) + else if (APIUtil.getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.requestHeaders)) { + logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature val dauthToken = DAuth.getDAuthToken(cc.requestHeaders) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index d9eea8893..e280a306f 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2721,8 +2721,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ Future { (Failure(ErrorMessages.GatewayLoginUnknownError), None) } } } // DAuth Login - else if (getPropsAsBoolValue("allow_dauth_login", false) && hasDAuthHeader(cc.requestHeaders)) { - logger.info("allow_dauth_login-getRemoteIpAddress: " + remoteIpAddress ) + else if (getPropsAsBoolValue("allow_dauth", false) && hasDAuthHeader(cc.requestHeaders)) { + logger.info("allow_dauth-getRemoteIpAddress: " + remoteIpAddress ) APIUtil.getPropsValue("dauth.host") match { case Full(h) if h.split(",").toList.exists(_.equalsIgnoreCase(remoteIpAddress) == true) => // Only addresses from white list can use this feature val dauthToken = DAuth.getDAuthToken(cc.requestHeaders) 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 4699d751b..b9bb44450 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2016,7 +2016,7 @@ object Glossary extends MdcLoggable { """) - val dauthEnabledMessage : String = if (APIUtil.getPropsAsBoolValue("allow_dauth_login", false)) + val dauthEnabledMessage : String = if (APIUtil.getPropsAsBoolValue("allow_dauth", false)) {"Note: DAuth is enabled."} else {"Note: *DAuth is NOT enabled on this instance!*"} @@ -2048,7 +2048,7 @@ object Glossary extends MdcLoggable { |# jwt.token_secret=your-at-least-256-bit-secret-token |# Enable/Disable DAuth communication at all |# In case isn't defined default value is false -|# allow_dauth_login=false +|# allow_dauth=false |# Define comma separated list of allowed IP addresses |# dauth.host=127.0.0.1 |# -------------------------------------- DAuth-- @@ -2175,7 +2175,7 @@ object Glossary extends MdcLoggable { |We: | |``` -|-> Check if Props allow_dauth_login is true +|-> Check if Props allow_dauth is true | -> Check if DAuth header exists | -> Check if getRemoteIpAddress is OK | -> Look for "token" diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index ec0546e8b..8a26eeefe 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -45,7 +45,7 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("migration_scripts.execute_all" -> "true") setPropsValues("migration_scripts.execute" -> "true") - setPropsValues("allow_dauth_login" -> "true") + setPropsValues("allow_dauth" -> "true") setPropsValues("dauth.host" -> "127.0.0.1") setPropsValues("jwt.token_secret"->"your-at-least-256-bit-secret-token") From 18508254d81f75162289079c78fe13207717ee0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 9 Nov 2021 20:21:04 +0100 Subject: [PATCH 115/185] feature/DAuth; Add props jwt.public_key_rsa --- .../resources/props/sample.props.template | 13 +++++----- obp-api/src/main/scala/code/api/dauth.scala | 4 ++-- .../main/scala/code/api/util/JwtUtil.scala | 24 ++++++++++++++++++- .../src/test/scala/code/api/dauthTest.scala | 15 ++++++------ .../test/scala/code/setup/ServerSetup.scala | 5 ++++ 5 files changed, 43 insertions(+), 18 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 70f3225da..6ba33300b 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -678,14 +678,12 @@ autocomplete_at_login_form_enabled=false # control access to customer firehose. # allow_customer_firehose=false -# -- Gateway Login And DAuth common settings-------------------------------------- -# Define secret used to validate JWT token -# jwt.token_secret=your-at-least-256-bit-secret-token - # -- Gateway login -------------------------------------- # Enable/Disable Gateway communication at all # In case isn't defined default value is false # allow_gateway_login=false +# Define secret used to validate JWT token +# jwt.token_secret=your-at-least-256-bit-secret-token # Define comma separated list of allowed IP addresses # gateway.host=127.0.0.1 @@ -693,10 +691,11 @@ autocomplete_at_login_form_enabled=false # -- DAuth -------------------------------------- # Enable/Disable DAuth communication at all # In case isn't defined default value is false -# allow_dauth=false +allow_dauth=false +# Define public key used to validate JWT token +jwt.public_key_rsa=path-to-the-pem-file # Define comma separated list of allowed IP addresses -# dauth.host=127.0.0.1 - +dauth.host=127.0.0.1 # -------------------------------------- DAuth-- diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala index 4835b798a..b1c8e7fcb 100755 --- a/obp-api/src/main/scala/code/api/dauth.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -114,8 +114,8 @@ object DAuth extends RestHelper with MdcLoggable { Box(parse(claim.toString).extractOpt[PayloadOfJwtJSON]) case false => logger.debug("validateJwtToken says: verifying jwt token with HmacProtection: " + token) - logger.debug(CertificateUtil.verifywtWithHmacProtection(token).toString) - CertificateUtil.verifywtWithHmacProtection(token) match { + logger.debug(JwtUtil.validateJwtWithRsaKey(token).toString) + JwtUtil.validateJwtWithRsaKey(token) match { case true => logger.debug("validateJwtToken says: jwt is verified: " + token) val claim = CertificateUtil.parseJwtWithHmacProtection(token) diff --git a/obp-api/src/main/scala/code/api/util/JwtUtil.scala b/obp-api/src/main/scala/code/api/util/JwtUtil.scala index fa899d189..301d4e707 100644 --- a/obp-api/src/main/scala/code/api/util/JwtUtil.scala +++ b/obp-api/src/main/scala/code/api/util/JwtUtil.scala @@ -1,11 +1,14 @@ package code.api.util -import java.net.URL +import java.net.{URI, URL} +import java.nio.file.{Files, Paths} import java.text.ParseException +import code.api.util.RSAUtil.logger import code.util.Helper.MdcLoggable import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.crypto.{MACVerifier, RSASSAVerifier} +import com.nimbusds.jose.jwk.{JWK, RSAKey} import com.nimbusds.jose.jwk.source.{JWKSource, RemoteJWKSet} import com.nimbusds.jose.proc.{JWSVerificationKeySelector, SecurityContext} import com.nimbusds.jose.util.{DefaultResourceRetriever, JSONObjectUtils} @@ -235,7 +238,26 @@ object JwtUtil extends MdcLoggable { } } + def validateJwtWithRsaKey(jwtString: String): Boolean = { + val relativePath = APIUtil.getPropsValue("jwt.public_key_rsa", "") + val basePath = this.getClass.getResource("/").toString .replaceFirst("target[/\\\\].*$", "") + val filePath = new URI(s"${basePath}/$relativePath").getPath + val publicKey = getPublicRsaKeyFromFile(filePath) + val signedJWT = SignedJWT.parse(jwtString) + val verifier = new RSASSAVerifier(publicKey) + signedJWT.verify(verifier) + } + + def getPublicRsaKeyFromFile(path: String): RSAKey = { + val pathOfFile = Paths.get(path) + val pemEncodedRSAPubliceKey = Files.readAllLines(pathOfFile).toArray.toList.mkString("\n") + logger.debug(pemEncodedRSAPubliceKey) + // Parse PEM-encoded key to RSA public / private JWK + val jwk: JWK = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPubliceKey); + logger.debug(s"Key $path is private: " + jwk.isPrivate) + jwk.toPublicJWK.toRSAKey + } def main(args: Array[String]): Unit = { diff --git a/obp-api/src/test/scala/code/api/dauthTest.scala b/obp-api/src/test/scala/code/api/dauthTest.scala index 84006f700..d00c821aa 100644 --- a/obp-api/src/test/scala/code/api/dauthTest.scala +++ b/obp-api/src/test/scala/code/api/dauthTest.scala @@ -1,6 +1,5 @@ package code.api -import code.api.util.APIUtil.OAuth._ import code.api.util.ErrorMessages import code.setup.{DefaultUsers, PropsReset, ServerSetup} import org.scalatest._ @@ -18,7 +17,7 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P "request_id": "0Xe876987694328763492876348928736497869273649" } */ - val invalidSecretJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfa2V5IjoiMHgxOTI1NWE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZXN0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJyZXF1ZXN0X2lkIjoiMFhlODc2OTg3Njk0MzI4NzYzNDkyODc2MzQ4OTI4NzM2NDk3ODY5MjczNjQ5In0.mK4Bx-V3reGe2jWxvQ5NQNSLXZ7AVRTX2fUFLD-2sSs" + val wrongPublicKeyJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfa2V5IjoiMHgxOTI1NWE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZXN0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJyZXF1ZXN0X2lkIjoiMFhlODc2OTg3Njk0MzI4NzYzNDkyODc2MzQ4OTI4NzM2NDk3ODY5MjczNjQ5In0.Wa1aaoHKSWAKcX_tnueVf3PQze-BcPeZ_EfhovxRvv9WIGn86WSShT2x2W_VGfySYJhfYYhpg2N-l-trA2T9jru5u3Mp_yQcSJZ9D1kCg3kmy2AqYp_qbPIakVQthWo1Ys7hkGB6bZHau87BOXv9v4v97LrpRfva5lw62qzdhpN67lTK1hdUc677nsneFdtnA78Ddm6u_ta_KIf_mC0t-lxSfUcuLb7LQgp2biYyYMgVB7dyexPQ7ZSBa2B8ARGXBXo0iOCjvi-Su4IYUomklRwKWYI-waaigaCDd9FZZQyDfjEySQToAG7UO0mPBRQiIVrSumecz1VESlO_c0Bm0w" /* Payload data. verified by correct secret "your-at-least-256-bit-secret-token" { @@ -30,9 +29,9 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P "request_id": "0Xe876987694328763492876348928736497869273649" } */ - val jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyNCIsIm5ldHdvcmtfbmFtZSI6IkVUSEVSRVVNIiwibXNnX3NlbmRlciI6IjB4ZTkwOTgwOTI3ZjE3MjVFNzczNENFMjg4RjgzNjdlMUJiMTQzRTkwZmhrdTc2NyIsImNvbnN1bWVyX2tleSI6IjB4MTkyNTVhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMTgtMDgtMjBUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.Wg2BYYsbWK-MUYToeTUc0GvDwcwnkR6Dh4SV-pMjChk" + val jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyIiwibmV0d29ya19uYW1lIjoiRVRIRVJFVU0iLCJtc2dfc2VuZGVyIjoiMHhlOTA5ODA5MjdmMTcyNUU3NzM0Q0UyODhGODM2N2UxQmIxNDNFOTBmaGt1NzY3IiwiY29uc3VtZXJfa2V5IjoiMHgxOTI1NWE0ZWMzMWU4OWNlYTU0ZDFmMTI1ZGI3NTM2ZTg3NGFiNGE5NmI0ZDRmNjQzODY2OGI2YmIxMGE2YWRiIiwidGltZXN0YW1wIjoiMjAxOC0wOC0yMFQxNDoxMzo0MFoiLCJyZXF1ZXN0X2lkIjoiMFhlODc2OTg3Njk0MzI4NzYzNDkyODc2MzQ4OTI4NzM2NDk3ODY5MjczNjQ5In0.dkAy32AjskvOaQ-gzXEiwU7RslJIawrOPsFsrqAlGHeKr6NyLJPJLYQ6e8_ABK2N-Pw43PiIzefV5QdiGxtWXCuVMRldrdNVC2VdBLVicDVWOmHCLyQ-mFbUvBR3wx8ZsU9nauEchVBsI9UY-_YYYI4yF9DsUazdMoesIjDl-zr68Dzm_ljnxv1wL4fbFpT7wq7MRFQBSy5UTN9o0JxGN_sm9dYeGf-kINQP8-zmJKQM0CRlMegdcBJdonSjlJDib_cKdbyeiSYwWTnqu9pAsOKarY7sX7uIa4A2hVkGY9hkSaGoeQcTxUHFTrJFdEeDm2num2MNLjFul3roAEG0Uw" - val invalidJwt = ("DAuth", ("%s").format(invalidSecretJwt)) + val invalidJwt = ("DAuth", ("%s").format(wrongPublicKeyJwt)) val validJwt = ("DAuth", ("%s").format(jwt)) def dauthRequest = baseRequest / "obp" / "v2.0.0" / "users" /"current" @@ -47,7 +46,7 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P val responseInvalid = makeGetRequest(dauthRequest, List(invalidJwt)) Then("We should get a 400 - Bad Request") logger.debug("-----------------------------------------") - logger.debug("responseInvalid response: "+responseInvalid) + logger.debug("responseInvalid response: "+ responseInvalid) logger.debug("-----------------------------------------") responseInvalid.code should equal(400) responseInvalid.toString contains (ErrorMessages.DAuthJwtTokenIsNotValid) should be (true) @@ -55,7 +54,7 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P When("We try to login with an valid JWT") val responseValidJwt = makeGetRequest(dauthRequest, List(validJwt)) logger.debug("-----------------------------------------") - logger.debug("responseValidJwt response: "+responseValidJwt) + logger.debug("responseValidJwt response: " + responseValidJwt) logger.debug("-----------------------------------------") responseValidJwt.code should equal(200) @@ -63,7 +62,7 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P val responseNonBlockingInvalid = makeGetRequest(dauthNonBlockingRequest, List(invalidJwt)) Then("We should get a 400 - Bad Request") logger.debug("-----------------------------------------") - logger.debug("responseNonBlockingInvalid responseNonBlocking: "+responseNonBlockingInvalid) + logger.debug("responseNonBlockingInvalid responseNonBlocking: " + responseNonBlockingInvalid) logger.debug("-----------------------------------------") responseNonBlockingInvalid.code should equal(401) responseNonBlockingInvalid.toString contains (ErrorMessages.DAuthJwtTokenIsNotValid) should be (true) @@ -71,7 +70,7 @@ class dauthTest extends ServerSetup with BeforeAndAfter with DefaultUsers with P When("We try to login with an valid JWT") val responseNonBlockingValidJwt = makeGetRequest(dauthNonBlockingRequest, List(validJwt)) logger.debug("-----------------------------------------") - logger.debug("responseNonBlockingValidJwt responseNonBlocking: "+responseNonBlockingValidJwt) + logger.debug("responseNonBlockingValidJwt responseNonBlocking: " + responseNonBlockingValidJwt) logger.debug("-----------------------------------------") responseValidJwt.code should equal(200) } diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 8a26eeefe..57562d71f 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -27,6 +27,8 @@ TESOBE (http://www.tesobe.com/) package code.setup +import java.net.URI + import _root_.net.liftweb.json.JsonAST.JObject import code.TestServer import code.api.util.APIUtil._ @@ -48,6 +50,9 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("allow_dauth" -> "true") setPropsValues("dauth.host" -> "127.0.0.1") setPropsValues("jwt.token_secret"->"your-at-least-256-bit-secret-token") + val basePath = this.getClass.getResource("/").toString .replaceFirst("target[/\\\\].*$", "") + val filePath = new URI(s"${basePath}/src/test/resources/cert/public_dauth.pem").getPath + setPropsValues("jwt.public_key_rsa" -> filePath) val server = TestServer def baseRequest = host(server.host, server.port) From 606d665bcb0f35d36267645cc92cdeef676c8047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 10 Nov 2021 08:56:24 +0100 Subject: [PATCH 116/185] test/DAuth; Add file public_dauth.pem --- .../src/main/scala/code/api/util/JwtUtil.scala | 2 +- .../src/test/resources/cert/public_dauth.pem | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 obp-api/src/test/resources/cert/public_dauth.pem diff --git a/obp-api/src/main/scala/code/api/util/JwtUtil.scala b/obp-api/src/main/scala/code/api/util/JwtUtil.scala index 301d4e707..b82b44cea 100644 --- a/obp-api/src/main/scala/code/api/util/JwtUtil.scala +++ b/obp-api/src/main/scala/code/api/util/JwtUtil.scala @@ -241,7 +241,7 @@ object JwtUtil extends MdcLoggable { def validateJwtWithRsaKey(jwtString: String): Boolean = { val relativePath = APIUtil.getPropsValue("jwt.public_key_rsa", "") val basePath = this.getClass.getResource("/").toString .replaceFirst("target[/\\\\].*$", "") - val filePath = new URI(s"${basePath}/$relativePath").getPath + val filePath = new URI(s"${basePath}$relativePath").getPath val publicKey = getPublicRsaKeyFromFile(filePath) val signedJWT = SignedJWT.parse(jwtString) val verifier = new RSASSAVerifier(publicKey) diff --git a/obp-api/src/test/resources/cert/public_dauth.pem b/obp-api/src/test/resources/cert/public_dauth.pem new file mode 100644 index 000000000..d6562bc8a --- /dev/null +++ b/obp-api/src/test/resources/cert/public_dauth.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICsjCCAZqgAwIBAgIGAX0EbA7DMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMM +D2FwcC5leGFtcGxlLmNvbTAeFw0yMTExMDkxMTE4NTBaFw0yMzExMDkxMTE4NTBa +MBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALBf/GQFAiT3c6eeiNbF+G3qHgy5xPOHrgsCr13Vkh2CL5x1 +AoIG7eHSDTZFcIrW7cescf6KEqYSQil5Z64GiwxbaXT3CPZI6wHpc7S0FD89WQVQ +yeJSUr8me0LLOQ5YCmnJ6fn41X4K2PCekJx86vgF4Wbgw5+35Ud9vTxWYVkM247i +O4MdiVR9fd53o9eAOsVJxSJ8ITw41tomqp0h6Wuam9MMuEOIwo48KSMAR+Lx2XmF +9nUIAsxyVFhYibSVT0lnt2OPtm9x/PYEswC0HSM2YNKb31gOyDrGBwrLCDXRArnk +dBSWeK63n85LtK2Th/rf6Qy01aG4e+LFx3f6YI0CAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAonMKkbeqcUOdzqRQ4yE3Did3yM3oxaN2vuB0g4QQ85u++xdGNl5wnl9j +xDzkkvjgO8x1YHsfF0+YoOxF04ObQkZqG8NJUNazPIkRwkEUj5QFQ9YA9uoShaS6 +78w+eXunb6BkPuPsj/nzcB8loUrBgUwfB3NvUx9oCL6snB4iuS7PULtSZ894kXi+ +lyk8C3uuY4o050CQ23LmNtV49yTrzYpE3sLtx3s8u3MxcEEDazvKW9x0o4/Y2sSx +iN7YfI2sF3Jldp2zrlWIoboQMnrJoT5KmKpolRmlR5iEl23fYmzi7Hn+piXrvmS/ +IiACsMLX56d+ggduyvrWnqiWPjvKDA== +-----END CERTIFICATE----- \ No newline at end of file From 895f44f9acd27375a59e58adbeb8934a2f5e36e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 10 Nov 2021 09:23:05 +0100 Subject: [PATCH 117/185] test/Tweak value of the props jwt.public_key_rsa --- obp-api/src/test/scala/code/setup/ServerSetup.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 57562d71f..2a77bc0ac 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -50,9 +50,7 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("allow_dauth" -> "true") setPropsValues("dauth.host" -> "127.0.0.1") setPropsValues("jwt.token_secret"->"your-at-least-256-bit-secret-token") - val basePath = this.getClass.getResource("/").toString .replaceFirst("target[/\\\\].*$", "") - val filePath = new URI(s"${basePath}/src/test/resources/cert/public_dauth.pem").getPath - setPropsValues("jwt.public_key_rsa" -> filePath) + setPropsValues("jwt.public_key_rsa" -> "src/test/resources/cert/public_dauth.pem") val server = TestServer def baseRequest = host(server.host, server.port) From ce862ef94bf846ead8843f0aa696a0baaaf618b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 10 Nov 2021 10:52:01 +0100 Subject: [PATCH 118/185] docfix/Tweak documentation at endpoint createHistoricalTransactionAtBank --- .../scala/code/api/v4_0_0/APIMethods400.scala | 49 +------------------ 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 4c61c0ed2..a6a1fba44 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -4731,53 +4731,8 @@ trait APIMethods400 { |So: |When you need the account to account, just omit counterparty_id field.eg: |{ - | "from": { - | "bank_id": "gh.29.uk", - | "account_id": "1ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", - | }, - | "to": { - | "bank_id": "gh.29.uk", - | "account_id": "2ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", - | }, - | "value": { - | "currency": "GBP", - | "amount": "10" - | }, - | "description": "this is for work", - | "posted": "2017-09-19T02:31:05Z", - | "completed": "2017-09-19T02:31:05Z", - | "type": "SANDBOX_TAN", - | "charge_policy": "SHARED" - |} - | - |When you need the counterparty to counterparty, need to omit bank_id and account_id field.eg: - |{ - | "from": { - | "counterparty_id": "f6392b7d-4218-45ea-b9a7-eaa71c0202f9" - | }, - | "to": { - | "counterparty_id": "26392b7d-4218-45ea-b9a7-eaa71c0202f9" - | }, - | "value": { - | "currency": "GBP", - | "amount": "10" - | }, - | "description": "this is for work", - | "posted": "2017-09-19T02:31:05Z", - | "completed": "2017-09-19T02:31:05Z", - | "type": "SANDBOX_TAN", - | "charge_policy": "SHARED" - |} - | - |or, you can counterparty to account - |{ - | "from": { - | "counterparty_id": "f6392b7d-4218-45ea-b9a7-eaa71c0202f9" - | }, - | "to": { - | "bank_id": "gh.29.uk", - | "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", - | }, + | "from_account_id": "1ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", + | "to_account_id": "2ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", | "value": { | "currency": "GBP", | "amount": "10" From 1d02faf4c5b7ef44554673ec61ebf83a7cd7e6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 10 Nov 2021 11:09:15 +0100 Subject: [PATCH 119/185] docfix/Tweak documentation at endpoint createHistoricalTransactionAtBank 2 --- obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index a6a1fba44..5b262f84a 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -4723,11 +4723,10 @@ trait APIMethods400 { "/banks/BANK_ID/management/historical/transactions", "Create Historical Transactions ", s""" - |Import the historical transactions. + |Create historical transactions at one Bank | - |The fields bank_id, account_id, counterparty_id in the json body are all optional ones. - |It support transfer money from account to account, account to counterparty and counterparty to counterparty - |Both bank_id + account_id and counterparty_id can identify the account, so OBP only need one of them to make the payment. + |Use this endpoint to create transactions between any two accounts at the same bank. + |From account and to account must be at the same bank. |So: |When you need the account to account, just omit counterparty_id field.eg: |{ From 94001269a9aed76ea6b854a794ec32ebc8b2520d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 10 Nov 2021 11:17:12 +0100 Subject: [PATCH 120/185] docfix/Tweak documentation at endpoint createHistoricalTransactionAtBank 3 --- obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 5b262f84a..49d4cfc75 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -4727,8 +4727,7 @@ trait APIMethods400 { | |Use this endpoint to create transactions between any two accounts at the same bank. |From account and to account must be at the same bank. - |So: - |When you need the account to account, just omit counterparty_id field.eg: + |Example: |{ | "from_account_id": "1ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", | "to_account_id": "2ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", From ed9a2aa1a48a542cc0340fa321ba073042eef396 Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 10 Nov 2021 17:13:16 +0100 Subject: [PATCH 121/185] refactor/use the callContext.userId to prepare the headers --- .../code/bankconnectors/rest/RestConnector_vMar2019.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index 1d9c6dc33..c412085f6 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -42,6 +42,7 @@ import code.api.util.{APIUtil, CallContext, OBPQueryParam} import code.api.v4_0_0.dynamic.MockResponseHolder import code.bankconnectors._ import code.bankconnectors.vJune2017.AuthInfo +import code.context.UserAuthContextProvider import code.customer.internalMapping.MappedCustomerIdMappingProvider import code.kafka.KafkaHelper import code.model.dataAccess.internalMapping.MappedAccountIdMappingProvider @@ -6581,10 +6582,10 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable ): List[HttpHeader] = { val needSignatureHead = APIUtil.getPropsAsBoolValue("rest_connector_sends_x-sign_header", false) - val generalContext = callContext.flatMap(_.toOutboundAdapterCallContext.generalContext).getOrElse(List.empty[BasicGeneralContext]) + val generalContext = callContext.map(createBasicUserAuthContextJsonFromCallContext(_)).getOrElse(List.empty[BasicGeneralContext]) val headersFromGeneralContext = generalContext.map(generalContext => RawHeader(generalContext.key,generalContext.value)) - val basicUserAuthContexts: List[BasicUserAuthContext] = callContext.flatMap(_.toOutboundAdapterCallContext.outboundAdapterAuthInfo.flatMap(_.userAuthContext)).getOrElse(List.empty[BasicUserAuthContext]) + val basicUserAuthContexts = UserAuthContextProvider.userAuthContextProvider.vend.getUserAuthContextsBox(callContext.map(_.userId).getOrElse("")).getOrElse(List.empty[UserAuthContext]) val headersFromUserAuthContext = basicUserAuthContexts.filterNot(_.key == "private-key").map(userAuthContext =>RawHeader(userAuthContext.key,userAuthContext.value)) val timeStamp = Instant.now.getEpochSecond.toString From f7b50239e0502c22827877f57a552db59b7de522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 11 Nov 2021 07:52:09 +0100 Subject: [PATCH 122/185] docfix/Tweak DAuth glossary documentation --- .../main/scala/code/api/util/Glossary.scala | 35 +++---------------- 1 file changed, 5 insertions(+), 30 deletions(-) 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 b9bb44450..10a560b8a 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2045,7 +2045,7 @@ object Glossary extends MdcLoggable { |``` |# -- DAuth -------------------------------------- |# Define secret used to validate JWT token -|# jwt.token_secret=your-at-least-256-bit-secret-token +|# jwt.public_key_rsa=path-to-the-pem-file |# Enable/Disable DAuth communication at all |# In case isn't defined default value is false |# allow_dauth=false @@ -2053,11 +2053,12 @@ object Glossary extends MdcLoggable { |# dauth.host=127.0.0.1 |# -------------------------------------- DAuth-- |``` -|Please keep in mind that property jwt.token_secret is used to validate JWT token to check it is not changed or corrupted during transport. +|Please keep in mind that property jwt.public_key_rsa is used to validate JWT token to check it is not changed or corrupted during transport. | |### 2) Create / have access to a JWT | -| +|The following videos are available: +| * [DAuth in local environment](https://vimeo.com/644315074) | |HEADER:ALGORITHM & TOKEN TYPE | @@ -2110,7 +2111,7 @@ object Glossary extends MdcLoggable { | |Headers: | -| Authorization: DAuth token="your-jwt-from-step-above" +| DAuth: your-jwt-from-step-above | |Here is it all together: | @@ -2142,32 +2143,6 @@ object Glossary extends MdcLoggable { |} |``` | -|### Example python script -|``` -|import jwt -|from datetime import datetime, timezone -|import requests -| -|env = 'local' -|DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' -| -|obp_api_host = '$getServerUrl' -|payload = { -| "smart_contract_address": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", -| "network_name": "ETHEREUM", -| "consumer_key": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", -| "timestamp": datetime.now(timezone.utc).strftime(DATE_FORMAT), -| "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", -| "request_id": "0Xe876987694328763492876348928736497869273649" -|} -| -|token = jwt.encode(payload, 'your-at-least-256-bit-secret-token', algorithm='HS256').decode("utf-8") -|headers = {'DAuth': token} -|url = obp_api_host + '/obp/v4.0.0/users/current' -|req = requests.get(url, headers=headers) -|print(req.text) -|``` -| |### Under the hood | |The file, dauth.scala handles the DAuth, From dce6d78e428572ee228bad3ff62ce5a54456cbfc Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 12 Nov 2021 10:14:18 +0000 Subject: [PATCH 123/185] Tweaking DAuth glossary item --- .../main/scala/code/api/util/Glossary.scala | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) 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 10a560b8a..e9a9e6b8e 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2024,23 +2024,38 @@ object Glossary extends MdcLoggable { title = APIUtil.DAuthHeaderKey, description = s""" - |### Introduction + |### DAuth Introduction, Setup and Usage +| +| +|DAuth is an experimental authentication mechanism that aims to pin an ethereum or other blockchain Smart Contract to an OBP "User". +| +|In the future, it might be possible to be more specific and pin specific actors (wallets) that are acting within the smart contract, but so far, one smart contract acts on behalf of one User. +| +|Thus, if a smart contract "X" calls the OBP API using the DAuth header, OBP will get or create a user called X and the call will proceed in the context of that User "X". +| +| +|DAuth is invoked by the REST client (caller) including a specific header (see step 3 below) in any OBP REST call. +| +|When OBP receives the DAuth token, it creates or gets a User with a username based on the smart_contract_address and the provider based on the network_name. The combination of username and provider is unique in OBP. +| +|If you are calling OBP-API via an API3 Airnode, the Airnode will take care of constructing the required header. +| +|When OBP detects an DAuth header / token it first checks if the Consumer is allowed to make such a call. OBP will validate the Consumer ip address and signature etc. +| +|Note: The DAuth flow does *not* require an explicit POST like Direct Login to create the token. +| +|Permissions may be assigned to an OBP User at any time, via the UserAuthContext, Views, Entitlements to Roles or Consents. | |$dauthEnabledMessage | -|DAuth Authorisation is made by including a specific header (see step 3 below) in any OBP REST call. +|Note: *The DAuth client is responsible for creating a token which will be trusted by OBP absolutely*! | -|Note: DAuth does *not* require an explicit POST like Direct Login to create the token. -| -|The **DAuth is responsible** for creating a token which is trusted by OBP **absolutely**! -| -|When OBP receives a token via DAuth, OBP creates or gets a user based on the username (smart_contract_address) supplied. | |To use DAuth: | |### 1) Configure OBP API to accept DAuth. | -|Set up properties in a props file +|Set up properties in your props file | |``` |# -- DAuth -------------------------------------- @@ -2073,7 +2088,7 @@ object Glossary extends MdcLoggable { |``` |{ | "smart_contract_address": "0xe123425E7734CE288F8367e1Bb143E90bb3F051224", -| "network_name": "ETHEREUM", +| "network_name": "AIRNODE.TESTNET.ETHEREUM", | "msg_sender": "0xe12340927f1725E7734CE288F8367e1Bb143E90fhku767", | "consumer_key": "0x1234a4ec31e89cea54d1f125db7536e874ab4a96b4d4f6438668b6bb10a6adb", | "timestamp": "2021-11-04T14:13:40Z", @@ -2089,7 +2104,7 @@ object Glossary extends MdcLoggable { |) your-at-least-256-bit-secret-token |``` | -|Here is the above example token: +|Here is an example token: | |``` |eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbWFydF9jb250cmFjdF9hZGRyZXNzIjoiMHhlMTIzNDI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGJiM0YwNTEyMjQiLCJuZXR3b3JrX25hbWUiOiJFVEhFUkVVTSIsIm1zZ19zZW5kZXIiOiIweGUxMjM0MDkyN2YxNzI1RTc3MzRDRTI4OEY4MzY3ZTFCYjE0M0U5MGZoa3U3NjciLCJjb25zdW1lcl9rZXkiOiIweDEyMzRhNGVjMzFlODljZWE1NGQxZjEyNWRiNzUzNmU4NzRhYjRhOTZiNGQ0ZjY0Mzg2NjhiNmJiMTBhNmFkYiIsInRpbWVzdGFtcCI6IjIwMjEtMTEtMDRUMTQ6MTM6NDBaIiwicmVxdWVzdF9pZCI6IjBYZTg3Njk4NzY5NDMyODc2MzQ5Mjg3NjM0ODkyODczNjQ5Nzg2OTI3MzY0OSJ9.XSiQxjEVyCouf7zT8MubEKsbOBZuReGVhnt9uck6z6k From b72c4dbb4d4b6c064b4bb4ec04385f2ec0c75942 Mon Sep 17 00:00:00 2001 From: tawoe Date: Fri, 12 Nov 2021 15:17:32 +0100 Subject: [PATCH 124/185] docfix / dauth --- obp-api/src/main/scala/code/api/util/Glossary.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e9a9e6b8e..f6eda2e31 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2079,7 +2079,7 @@ object Glossary extends MdcLoggable { | |``` |{ -| "alg": "HS256", +| "alg": "RS256", | "typ": "JWT" |} |``` From ca31087565af830c38543462565a5d40d87bf577 Mon Sep 17 00:00:00 2001 From: tawoe Date: Fri, 12 Nov 2021 17:05:14 +0100 Subject: [PATCH 125/185] docfix / dauth --- obp-api/src/main/scala/code/api/util/Glossary.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f6eda2e31..77d2304be 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2097,7 +2097,7 @@ object Glossary extends MdcLoggable { |``` |VERIFY SIGNATURE |``` -|HMACSHA256( +|RSASHA256( | base64UrlEncode(header) + "." + | base64UrlEncode(payload), | From d93f622203ebf4256c43b3d13ddbe60558f24d85 Mon Sep 17 00:00:00 2001 From: tawoe Date: Fri, 12 Nov 2021 17:07:15 +0100 Subject: [PATCH 126/185] docfix / dauth --- obp-api/src/main/scala/code/api/util/Glossary.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 77d2304be..4be54dfca 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2101,7 +2101,7 @@ object Glossary extends MdcLoggable { | base64UrlEncode(header) + "." + | base64UrlEncode(payload), | -|) your-at-least-256-bit-secret-token +|) your-RSA-key-pair |``` | |Here is an example token: From aa300e71259549e722306ee7ff8429730afd32f6 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 15 Nov 2021 17:29:03 +0100 Subject: [PATCH 127/185] feature/OBPv400 added new endpoint createUserWithAccountAccess --- .../SwaggerDefinitionsJSON.scala | 5 ++ .../scala/code/api/util/ErrorMessages.scala | 1 + .../main/scala/code/api/util/NewStyle.scala | 18 +++++ .../scala/code/api/v4_0_0/APIMethods400.scala | 77 +++++++++++++++++-- .../code/api/v4_0_0/JSONFactory4.0.0.scala | 1 + 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index fd0d420d4..4d62411bd 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4088,6 +4088,11 @@ object SwaggerDefinitionsJSON { ) val postAccountAccessJsonV400 = PostAccountAccessJsonV400(userIdExample.value, PostViewJsonV400(ExampleValue.viewIdExample.value, true)) + val postCreateUserAccountAccessJsonV400 = PostCreateUserAccountAccessJsonV400( + userIdExample.value, + providerExample.value, + List(PostViewJsonV400(ExampleValue.viewIdExample.value, true)) + ) val revokedJsonV400 = RevokedJsonV400(true) val postRevokeGrantAccountAccessJsonV400 = PostRevokeGrantAccountAccessJsonV400(List("ReadAccountsBasic")) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 50a057f4b..1d3dc1c11 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -192,6 +192,7 @@ object ErrorMessages { val InvalidDAuthHeaderToken = "OBP-20072: DAuth Header value should be one single string." val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " + val CannotGetOrCreateUser = "OBP-20102: Cannot get or create user." // OAuth 2 val ApplicationNotIdentified = "OBP-20200: The application cannot be identified. " diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 72648d7fa..f3ac7659a 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -1034,6 +1034,24 @@ object NewStyle { } } + def getOrCreateUser(userId: String, provider: String, callContext: Option[CallContext]): OBPReturnType[User] = { + Future { UserX.findByUserId(userId).or( //first try to find the user by userId + Users.users.vend.createResourceUser( // Otherwise create a new user + provider = provider, + providerId = Some(userId), + None, + name = Some(userId), + email = None, + userId = Some(userId), + createdByUserInvitationId = None, + company = None, + lastMarketingAgreementSignedDate = None + ) + ).map(user =>(user, callContext))} map { + unboxFullOrFail(_, callContext, s"$CannotGetOrCreateUser Current USER_ID($userId) PROVIDER ($provider)", 404) + } + } + def createTransactionRequestv210( u: User, viewId: ViewId, diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 49d4cfc75..1bef54f8b 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -4154,14 +4154,8 @@ trait APIMethods400 { } _ <- NewStyle.function.canGrantAccessToView(bankId, accountId, cc.loggedInUser, cc.callContext) (user, callContext) <- NewStyle.function.findByUserId(postJson.user_id, cc.callContext) - view <- postJson.view.is_system match { - case true => NewStyle.function.systemView(ViewId(postJson.view.view_id), callContext) - case false => NewStyle.function.customView(ViewId(postJson.view.view_id), BankIdAccountId(bankId, accountId), callContext) - } - addedView <- postJson.view.is_system match { - case true => NewStyle.function.grantAccessToSystemView(bankId, accountId, view, user, callContext) - case false => NewStyle.function.grantAccessToCustomView(view, user, callContext) - } + view <- checkView(bankId, accountId, postJson.view, callContext) + addedView <- createAccountAccessToUser(bankId, accountId, user, view, callContext) } yield { val viewJson = JSONFactory300.createViewJSON(addedView) (viewJson, HttpCode.`201`(callContext)) @@ -4169,6 +4163,50 @@ trait APIMethods400 { } } + staticResourceDocs += ResourceDoc( + createUserWithAccountAccess, + implementedInApiVersion, + nameOf(createUserWithAccountAccess), + "POST", + "/banks/BANK_ID/accounts/ACCOUNT_ID/user-account-access", + "Create User with Account Access", + s"""This will create the User with user_id and provider if it does not exist . + | + |${authenticationRequiredMessage(true)} and the loggedin user needs to be account holder. + | + |""", + postCreateUserAccountAccessJsonV400, + List(viewJsonV300), + List( + $UserNotLoggedIn, + UserMissOwnerViewOrNotAccountHolder, + InvalidJsonFormat, + SystemViewNotFound, + ViewNotFound, + CannotGrantAccountAccess, + UnknownError + ), + List(apiTagAccountAccess, apiTagView, apiTagAccount, apiTagUser, apiTagOwnerRequired, apiTagNewStyle)) + + lazy val createUserWithAccountAccess : OBPEndpoint = { + //add access for specific user to a specific system view + case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "user-account-access" :: Nil JsonPost json -> _ => { + cc => + val failMsg = s"$InvalidJsonFormat The Json body should be the $PostAccountAccessJsonV400 " + for { + postJson <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { + json.extract[PostCreateUserAccountAccessJsonV400] + } + _ <- NewStyle.function.canGrantAccessToView(bankId, accountId, cc.loggedInUser, cc.callContext) + (user, callContext) <- NewStyle.function.getOrCreateUser(postJson.user_id, postJson.provider, cc.callContext) + views <- checkViews(bankId, accountId, postJson, callContext) + addedView <- createAccountAccessesToUser(bankId, accountId, user, views, callContext) + } yield { + val viewsJson = addedView.map(JSONFactory300.createViewJSON(_)) + (viewsJson, HttpCode.`201`(callContext)) + } + } + } staticResourceDocs += ResourceDoc( revokeUserAccessToView, @@ -10741,6 +10779,29 @@ trait APIMethods400 { } + private def createAccountAccessToUser(bankId: BankId, accountId: AccountId, user: User, view: View, callContext: Option[CallContext]) = { + view.isSystem match { + case true => NewStyle.function.grantAccessToSystemView(bankId, accountId, view, user, callContext) + case false => NewStyle.function.grantAccessToCustomView(view, user, callContext) + } + } + private def createAccountAccessesToUser(bankId: BankId, accountId: AccountId, user: User, views: List[View], callContext: Option[CallContext]) = { + Future.sequence(views.map(view => + createAccountAccessToUser(bankId: BankId, accountId: AccountId, user: User, view, callContext: Option[CallContext]) + )) + } + + private def checkView(bankId: BankId, accountId: AccountId, postView: PostViewJsonV400, callContext: Option[CallContext]) = { + postView.is_system match { + case true => NewStyle.function.systemView(ViewId(postView.view_id), callContext) + case false => NewStyle.function.customView(ViewId(postView.view_id), BankIdAccountId(bankId, accountId), callContext) + } + } + + private def checkViews(bankId: BankId, accountId: AccountId, postJson: PostCreateUserAccountAccessJsonV400, callContext: Option[CallContext]) = { + Future.sequence(postJson.views.map(view => checkView(bankId: BankId, accountId: AccountId, view: PostViewJsonV400, callContext: Option[CallContext]))) + } + private def createDynamicEndpointMethod(bankId: Option[String], json: JValue, cc: CallContext) = { for { (postedJson, openAPI) <- NewStyle.function.tryons(InvalidJsonFormat, 400, cc.callContext) { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index 0c54094a0..1dfa471d5 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -355,6 +355,7 @@ case class StandingOrderJsonV400(standing_order_id: String, active: Boolean) case class PostViewJsonV400(view_id: String, is_system: Boolean) case class PostAccountAccessJsonV400(user_id: String, view: PostViewJsonV400) +case class PostCreateUserAccountAccessJsonV400(user_id: String, provider:String, views: List[PostViewJsonV400]) case class PostRevokeGrantAccountAccessJsonV400(views: List[String]) case class RevokedJsonV400(revoked: Boolean) From 6fc4fc3ada28dddd3865de6d702d0f619e41fd72 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 15 Nov 2021 17:37:21 +0100 Subject: [PATCH 128/185] test/OBPv400 added the tests for createUserWithAccountAccess --- .../scala/code/api/v4_0_0/AccountAccessTest.scala | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala index 285838a8f..6fe224600 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala @@ -28,6 +28,7 @@ class AccountAccessTest extends V400ServerSetup { object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.grantUserAccessToView)) object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.revokeUserAccessToView)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.createUserWithAccountAccess)) lazy val bankId = randomBankId @@ -70,7 +71,7 @@ class AccountAccessTest extends V400ServerSetup { } } - feature(s"test $ApiEndpoint1 and $ApiEndpoint2 version $VersionOfApi - Authorized access") { + feature(s"test $ApiEndpoint1 and $ApiEndpoint2 and $ApiEndpoint3 version $VersionOfApi - Authorized access") { scenario("We will call the endpoint with user credentials", VersionOfApi, ApiEndpoint1, ApiEndpoint2) { val addedEntitlement: Box[Entitlement] = Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, ApiRole.CanCreateAccount.toString) @@ -95,6 +96,17 @@ class AccountAccessTest extends V400ServerSetup { Then("We should get a 201 and check the response body") responseRevoke.code should equal(201) responseRevoke.body.extract[RevokedJsonV400] + + { + val postCreateUserJson = PostCreateUserAccountAccessJsonV400(resourceUser2.userId, resourceUser2.provider, List(PostViewJsonV400(view.id, view.is_system))) + When("We send the request") + val request = (v4_0_0_Request / "banks" / bankId / "accounts" / account.account_id / "user-account-access").POST <@ (user1) + val response = makePostRequest(request, write(postCreateUserJson)) + Then("We should get a 201 and check the response body") + response.code should equal(201) + val views = response.body.extract[List[ViewJsonV300]] + views.length + } } } From c5e20ed8e9fcacc156629519a3d07641b9ac33b1 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 15 Nov 2021 22:54:30 +0100 Subject: [PATCH 129/185] feature/OBPv400 added new endpoint -createUserWithRoles --- .../SwaggerDefinitionsJSON.scala | 5 + .../scala/code/api/v4_0_0/APIMethods400.scala | 128 +++++++++++++++++- .../code/api/v4_0_0/JSONFactory4.0.0.scala | 4 +- 3 files changed, 130 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 4d62411bd..0ed1a0c3d 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4093,6 +4093,11 @@ object SwaggerDefinitionsJSON { providerExample.value, List(PostViewJsonV400(ExampleValue.viewIdExample.value, true)) ) + val postCreateUserWithRolesJsonV400 = PostCreateUserWithRolesJsonV400( + userIdExample.value, + providerExample.value, + List(createEntitlementJSON) + ) val revokedJsonV400 = RevokedJsonV400(true) val postRevokeGrantAccountAccessJsonV400 = PostRevokeGrantAccountAccessJsonV400(List("ReadAccountsBasic")) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 1bef54f8b..356474df2 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -19,7 +19,7 @@ import code.api.v1_2_1.{JSONFactory, PostTransactionTagJSON} import code.api.v1_4_0.JSONFactory1_4_0 import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140 import code.api.v2_0_0.OBPAPI2_0_0.Implementations2_0_0 -import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200} +import code.api.v2_0_0.{CreateEntitlementJSON, EntitlementJSONs, JSONFactory200} import code.api.v2_1_0._ import code.api.v3_0_0.JSONFactory300 import code.api.v3_1_0._ @@ -73,7 +73,7 @@ import net.liftweb.json.JsonDSL._ import net.liftweb.json.Serialization.write import net.liftweb.json.{compactRender, prettyRender, _} import net.liftweb.mapper.By -import net.liftweb.util.Helpers.now +import net.liftweb.util.Helpers.{now, tryo} import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To, XHTMLMailBodyType} import net.liftweb.util.{Helpers, Mailer, StringHelpers} import org.apache.commons.collections4.CollectionUtils @@ -2735,7 +2735,72 @@ trait APIMethods400 { } } } - + staticResourceDocs += ResourceDoc( + createUserWithRoles, + implementedInApiVersion, + nameOf(createUserWithRoles), + "POST", + "/user-entitlements", + "Create User with Roles", + """This will create the User with user_id and provider if it does not exist + | + |Create Entitlement. Grant Role to User. + | + |Entitlements are used to grant System or Bank level roles to Users. (For Account level privileges, see Views) + | + |For a System level Role (.e.g CanGetAnyUser), set bank_id to an empty string i.e. "bank_id":"" + | + |For a Bank level Role (e.g. CanCreateAccount), set bank_id to a valid value e.g. "bank_id":"my-bank-id" + | + |Authentication is required and the user needs to be a Super Admin. Super Admins are listed in the Props file.""", + postCreateUserWithRolesJsonV400, + List(entitlementJSON), + List( + UserNotLoggedIn, + UserNotFoundById, + UserNotSuperAdmin, + InvalidJsonFormat, + IncorrectRoleName, + EntitlementIsBankRole, + EntitlementIsSystemRole, + EntitlementAlreadyExists, + UnknownError + ), + List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle), + Some(List(canCreateEntitlementAtOneBank,canCreateEntitlementAtAnyBank))) + + lazy val createUserWithRoles: OBPEndpoint = { + case "user-entitlements" :: Nil JsonPost json -> _ => { + cc => + for { + (Full(loggedInUser), callContext) <- authenticatedAccess(cc) + failMsg = s"$InvalidJsonFormat The Json body should be the $PostCreateUserWithRolesJsonV400 " + postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[PostCreateUserWithRolesJsonV400] + } + _ <- checkRolesBankId(callContext, postedData) + _ <- checkRolesBankIdExsiting(callContext, postedData) + roles <- checkRolesName(callContext, postedData) + (postBodyUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) + + allowedEntitlements = canCreateEntitlementAtAnyBank :: Nil + allowedEntitlementsTxt = UserNotSuperAdmin +" or" + UserHasMissingRoles + canCreateEntitlementAtAnyBank + _ <- + if(isSuperAdmin(loggedInUser.userId)) + Future.successful(Full(Unit)) + else + NewStyle.function.hasAtLeastOneEntitlement(allowedEntitlementsTxt)("", loggedInUser.userId, allowedEntitlements, callContext) + + _ <- checkIfUserHasEntitlements(postedData, callContext) + + addedEntitlements <- addEntitlementsToUser(postedData, callContext) + + } yield { + (addedEntitlements.map(JSONFactory200.createEntitlementJSON(_)), HttpCode.`201`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( getEntitlements, @@ -4175,7 +4240,7 @@ trait APIMethods400 { |${authenticationRequiredMessage(true)} and the loggedin user needs to be account holder. | |""", - postCreateUserAccountAccessJsonV400, + postCreateUserWithRolesJsonV400, List(viewJsonV300), List( $UserNotLoggedIn, @@ -4192,7 +4257,7 @@ trait APIMethods400 { //add access for specific user to a specific system view case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "user-account-access" :: Nil JsonPost json -> _ => { cc => - val failMsg = s"$InvalidJsonFormat The Json body should be the $PostAccountAccessJsonV400 " + val failMsg = s"$InvalidJsonFormat The Json body should be the $PostCreateUserAccountAccessJsonV400 " for { postJson <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { json.extract[PostCreateUserAccountAccessJsonV400] @@ -10779,6 +10844,59 @@ trait APIMethods400 { } + private def checkRoleBankIdExsiting(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { + Helper.booleanToFuture(failMsg = s"$BankNotFound Current BANK_ID (${entitlement.bank_id})", cc=callContext) { + entitlement.bank_id.nonEmpty == false || BankX(BankId(entitlement.bank_id), callContext).map(_._1).isEmpty == false + } + } + + private def checkRolesBankIdExsiting(callContext: Option[CallContext], postedData: PostCreateUserWithRolesJsonV400) = { + Future.sequence(postedData.roles.map(checkRoleBankIdExsiting(callContext,_))) + } + + private def addEntitlementToUser(userId:String, entitlement: CreateEntitlementJSON, callContext: Option[CallContext]) = { + Future(Entitlement.entitlement.vend.addEntitlement(entitlement.bank_id, userId, entitlement.role_name)) map { unboxFull(_) } + } + + private def addEntitlementsToUser(postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { + Future.sequence(postedData.roles.map(addEntitlementToUser(postedData.user_id, _, callContext))) + } + + private def checkIfUserHasEntitlement(userId:String, entitlement: CreateEntitlementJSON, callContext: Option[CallContext]) = { + Helper.booleanToFuture(failMsg = s"$EntitlementAlreadyExists Current Entitlement (${entitlement.role_name})", cc=callContext) { + hasEntitlement(entitlement.bank_id, userId, valueOf(entitlement.role_name)) == false + } + } + + private def checkIfUserHasEntitlements(postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { + Future.sequence(postedData.roles.map(checkIfUserHasEntitlement(postedData.user_id, _, callContext))) + } + + private def checkRoleBankIdRequirement(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { + Helper.booleanToFuture(failMsg = if (ApiRole.valueOf(entitlement.role_name).requiresBankId) EntitlementIsBankRole else EntitlementIsSystemRole, cc = callContext) { + ApiRole.valueOf(entitlement.role_name).requiresBankId == entitlement.bank_id.nonEmpty + } + } + + private def checkRolesBankId(callContext: Option[CallContext], postedData: PostCreateUserWithRolesJsonV400) = { + Future.sequence(postedData.roles.map(checkRoleBankIdRequirement(callContext,_))) + } + + private def checkRoleName(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { + Future{ + tryo { + valueOf(entitlement.role_name) + } + } map { + val msg = IncorrectRoleName + entitlement.role_name + ". Possible roles are " + ApiRole.availableRoles.sorted.mkString(", ") + x => unboxFullOrFail(x, callContext, msg) + } + } + + private def checkRolesName(callContext: Option[CallContext], postJsonBody: PostCreateUserWithRolesJsonV400) = { + Future.sequence(postJsonBody.roles.map(checkRoleName(callContext,_))) + } + private def createAccountAccessToUser(bankId: BankId, accountId: AccountId, user: User, view: View, callContext: Option[CallContext]) = { view.isSystem match { case true => NewStyle.function.grantAccessToSystemView(bankId, accountId, view, user, callContext) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index 1dfa471d5..3e59b2237 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -28,7 +28,6 @@ package code.api.v4_0_0 import java.text.SimpleDateFormat import java.util.Date - import code.api.attributedefinition.AttributeDefinition import code.api.util.APIUtil import code.api.util.APIUtil.{DateWithDay, DateWithSeconds, stringOptionOrNull, stringOrNull} @@ -36,7 +35,7 @@ import code.api.v1_2_1.JSONFactory.{createAmountOfMoneyJSON, createOwnersJSON} import code.api.v1_2_1.{BankRoutingJsonV121, JSONFactory, UserJSONV121, ViewJSONV121} import code.api.v1_4_0.JSONFactory1_4_0.{LocationJsonV140, MetaJsonV140, TransactionRequestAccountJsonV140, transformToLocationFromV140, transformToMetaFromV140} import code.api.v2_0_0.JSONFactory200.UserJsonV200 -import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200, TransactionRequestChargeJsonV200} +import code.api.v2_0_0.{CreateEntitlementJSON, EntitlementJSONs, JSONFactory200, TransactionRequestChargeJsonV200} import code.api.v2_1_0.{IbanJson, JSONFactory210, PostCounterpartyBespokeJson, ResourceUserJSON} import code.api.v2_2_0.CounterpartyMetadataJson import code.api.v3_0_0.JSONFactory300._ @@ -356,6 +355,7 @@ case class StandingOrderJsonV400(standing_order_id: String, case class PostViewJsonV400(view_id: String, is_system: Boolean) case class PostAccountAccessJsonV400(user_id: String, view: PostViewJsonV400) case class PostCreateUserAccountAccessJsonV400(user_id: String, provider:String, views: List[PostViewJsonV400]) +case class PostCreateUserWithRolesJsonV400(user_id: String, provider:String, roles: List[CreateEntitlementJSON]) case class PostRevokeGrantAccountAccessJsonV400(views: List[String]) case class RevokedJsonV400(revoked: Boolean) From 11e7a2bd9dce3396993c94851c201292f06b9b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 16 Nov 2021 11:08:41 +0100 Subject: [PATCH 130/185] feature/OpenID Connect; Enable props email_domain_to_space_mappings --- obp-api/src/main/scala/code/api/openidconnect.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/obp-api/src/main/scala/code/api/openidconnect.scala b/obp-api/src/main/scala/code/api/openidconnect.scala index f74d0129d..09f97ad98 100644 --- a/obp-api/src/main/scala/code/api/openidconnect.scala +++ b/obp-api/src/main/scala/code/api/openidconnect.scala @@ -122,6 +122,9 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { case Full(user) => // All good getOrCreateAuthUser(user) match { case Full(authUser) => + // Grant roles according to the props email_domain_to_space_mappings + AuthUser.grantEmailDomainEntitlementsToUser(authUser) + // Consumer getOrCreateConsumer(idToken, user.userId) match { case Full(consumer) => saveAuthorizationToken(tokenType, accessToken, idToken, refreshToken, scope, expiresIn, authUser.id.get) match { From 007c2226d5290a059af7bf07bbc78466da7d08ed Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 16 Nov 2021 13:22:39 +0100 Subject: [PATCH 131/185] test/OBPv400 added tests for createUserWithAccountAccess endpoint --- .../SwaggerDefinitionsJSON.scala | 11 +++++++ .../scala/code/api/v4_0_0/APIMethods400.scala | 6 ++-- .../code/api/v4_0_0/EntitlementTests.scala | 29 ++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 0ed1a0c3d..ec030e79e 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4493,6 +4493,17 @@ object SwaggerDefinitionsJSON { meta = metaJson, ) + val entitlementJsonV400 = EntitlementJsonV400( + entitlement_id = entitlementIdExample.value, + role_name = roleNameExample.value, + bank_id = bankIdExample.value, + user_id = userIdExample.value, + ) + + val entitlementsJsonV400 = EntitlementsJsonV400( + list = List(entitlementJsonV400) + ) + //The common error or success format. //Just some helper format to use in Json diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 356474df2..7d4e48f37 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -2796,7 +2796,7 @@ trait APIMethods400 { addedEntitlements <- addEntitlementsToUser(postedData, callContext) } yield { - (addedEntitlements.map(JSONFactory200.createEntitlementJSON(_)), HttpCode.`201`(callContext)) + (JSONFactory400.createEntitlementJSONs(addedEntitlements), HttpCode.`201`(callContext)) } } } @@ -2814,7 +2814,7 @@ trait APIMethods400 { | """.stripMargin, EmptyBody, - entitlementJSONs, + entitlementsJsonV400, List($UserNotLoggedIn, UserHasMissingRoles, UnknownError), List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle), Some(List(canGetEntitlementsForAnyUserAtAnyBank))) @@ -2851,7 +2851,7 @@ trait APIMethods400 { | """.stripMargin, EmptyBody, - entitlementJSONs, + entitlementsJsonV400, List($UserNotLoggedIn, UserHasMissingRoles, UnknownError), List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle), Some(List(canGetEntitlementsForOneBank,canGetEntitlementsForAnyBank))) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala index 79bf85e28..6b9b6f8c1 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala @@ -1,15 +1,19 @@ package code.api.v4_0_0 +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON import com.openbankproject.commons.model.ErrorMessage -import code.api.util.ApiRole.{CanGetEntitlementsForAnyBank, CanGetEntitlementsForAnyUserAtAnyBank, CanGetEntitlementsForOneBank} +import code.api.util.ApiRole.{CanCreateBranch, CanGetEntitlementsForAnyBank, CanGetEntitlementsForAnyUserAtAnyBank, CanGetEntitlementsForOneBank, CanUpdateBranch} import code.api.util.ErrorMessages.{UserHasMissingRoles, _} import code.api.util.{ApiRole, ErrorMessages} import code.entitlement.Entitlement import code.setup.DefaultUsers import code.api.util.APIUtil.OAuth._ +import code.api.util.ExampleValue.bankIdExample +import code.api.v2_0_0.CreateEntitlementJSON import code.api.v4_0_0.APIMethods400.Implementations4_0_0 import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.Serialization.write import org.scalatest.Tag class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { @@ -32,6 +36,7 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.getEntitlements)) object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getEntitlementsForBank)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.createUserWithRoles)) feature("Assuring that endpoint getEntitlements works as expected - v4.0.0") { @@ -109,6 +114,28 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { r.code should equal(200) } } + + scenario("We try to - createUserWithRoles", ApiEndpoint3, VersionOfApi) { + When("We add required entitlement") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanCreateEntitlementAtAnyBank.toString) + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + val entitlements = r.body.extract[EntitlementsJsonV400] + r.code should equal(201) + entitlements.list.length should be (2) + } + } } From b8f39ef5ecc331e2bf479e94cedd7acd6b68a2ca Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 16 Nov 2021 16:08:50 +0100 Subject: [PATCH 132/185] refactor/tweaked checkView -> getViews --- .../main/scala/code/api/v4_0_0/APIMethods400.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 7d4e48f37..a550472ed 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -4219,7 +4219,7 @@ trait APIMethods400 { } _ <- NewStyle.function.canGrantAccessToView(bankId, accountId, cc.loggedInUser, cc.callContext) (user, callContext) <- NewStyle.function.findByUserId(postJson.user_id, cc.callContext) - view <- checkView(bankId, accountId, postJson.view, callContext) + view <- getViews(bankId, accountId, postJson.view, callContext) addedView <- createAccountAccessToUser(bankId, accountId, user, view, callContext) } yield { val viewJson = JSONFactory300.createViewJSON(addedView) @@ -4254,7 +4254,6 @@ trait APIMethods400 { List(apiTagAccountAccess, apiTagView, apiTagAccount, apiTagUser, apiTagOwnerRequired, apiTagNewStyle)) lazy val createUserWithAccountAccess : OBPEndpoint = { - //add access for specific user to a specific system view case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "user-account-access" :: Nil JsonPost json -> _ => { cc => val failMsg = s"$InvalidJsonFormat The Json body should be the $PostCreateUserAccountAccessJsonV400 " @@ -4264,7 +4263,7 @@ trait APIMethods400 { } _ <- NewStyle.function.canGrantAccessToView(bankId, accountId, cc.loggedInUser, cc.callContext) (user, callContext) <- NewStyle.function.getOrCreateUser(postJson.user_id, postJson.provider, cc.callContext) - views <- checkViews(bankId, accountId, postJson, callContext) + views <- getViews(bankId, accountId, postJson, callContext) addedView <- createAccountAccessesToUser(bankId, accountId, user, views, callContext) } yield { val viewsJson = addedView.map(JSONFactory300.createViewJSON(_)) @@ -10909,15 +10908,15 @@ trait APIMethods400 { )) } - private def checkView(bankId: BankId, accountId: AccountId, postView: PostViewJsonV400, callContext: Option[CallContext]) = { + private def getView(bankId: BankId, accountId: AccountId, postView: PostViewJsonV400, callContext: Option[CallContext]) = { postView.is_system match { case true => NewStyle.function.systemView(ViewId(postView.view_id), callContext) case false => NewStyle.function.customView(ViewId(postView.view_id), BankIdAccountId(bankId, accountId), callContext) } } - private def checkViews(bankId: BankId, accountId: AccountId, postJson: PostCreateUserAccountAccessJsonV400, callContext: Option[CallContext]) = { - Future.sequence(postJson.views.map(view => checkView(bankId: BankId, accountId: AccountId, view: PostViewJsonV400, callContext: Option[CallContext]))) + private def getViews(bankId: BankId, accountId: AccountId, postJson: PostCreateUserAccountAccessJsonV400, callContext: Option[CallContext]) = { + Future.sequence(postJson.views.map(view => getView(bankId: BankId, accountId: AccountId, view: PostViewJsonV400, callContext: Option[CallContext]))) } private def createDynamicEndpointMethod(bankId: Option[String], json: JValue, cc: CallContext) = { From 25924c06c71a07fba9b1ebc33f5957a17b0ea610 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 16 Nov 2021 16:45:24 +0100 Subject: [PATCH 133/185] refactor/tweaked the method name checkIfUserHasEntitlements->checkIfUserAlreadyHasEntitlements --- .../scala/code/api/v4_0_0/APIMethods400.scala | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index a550472ed..9126f803f 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -2754,7 +2754,7 @@ trait APIMethods400 { | |Authentication is required and the user needs to be a Super Admin. Super Admins are listed in the Props file.""", postCreateUserWithRolesJsonV400, - List(entitlementJSON), + entitlementsJsonV400, List( UserNotLoggedIn, UserNotFoundById, @@ -2778,10 +2778,12 @@ trait APIMethods400 { postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { json.extract[PostCreateUserWithRolesJsonV400] } - _ <- checkRolesBankId(callContext, postedData) + //check the system role bankId is Empty, but bank level role need bankId + _ <- checkRoleBankIdMappings(callContext, postedData) + _ <- checkRolesBankIdExsiting(callContext, postedData) - roles <- checkRolesName(callContext, postedData) - (postBodyUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) + + _ <- checkRolesName(callContext, postedData) allowedEntitlements = canCreateEntitlementAtAnyBank :: Nil allowedEntitlementsTxt = UserNotSuperAdmin +" or" + UserHasMissingRoles + canCreateEntitlementAtAnyBank @@ -2790,8 +2792,10 @@ trait APIMethods400 { Future.successful(Full(Unit)) else NewStyle.function.hasAtLeastOneEntitlement(allowedEntitlementsTxt)("", loggedInUser.userId, allowedEntitlements, callContext) - - _ <- checkIfUserHasEntitlements(postedData, callContext) + + (postBodyUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) + + _ <- checkIfUserAlreadyHasEntitlements(postedData, callContext) addedEntitlements <- addEntitlementsToUser(postedData, callContext) @@ -10861,24 +10865,24 @@ trait APIMethods400 { Future.sequence(postedData.roles.map(addEntitlementToUser(postedData.user_id, _, callContext))) } - private def checkIfUserHasEntitlement(userId:String, entitlement: CreateEntitlementJSON, callContext: Option[CallContext]) = { + private def checkIfUserAlreadyHasEntitlement(userId:String, entitlement: CreateEntitlementJSON, callContext: Option[CallContext]) = { Helper.booleanToFuture(failMsg = s"$EntitlementAlreadyExists Current Entitlement (${entitlement.role_name})", cc=callContext) { hasEntitlement(entitlement.bank_id, userId, valueOf(entitlement.role_name)) == false } } - private def checkIfUserHasEntitlements(postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { - Future.sequence(postedData.roles.map(checkIfUserHasEntitlement(postedData.user_id, _, callContext))) + private def checkIfUserAlreadyHasEntitlements(postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { + Future.sequence(postedData.roles.map(checkIfUserAlreadyHasEntitlement(postedData.user_id, _, callContext))) } - private def checkRoleBankIdRequirement(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { + private def checkRoleBankIdMapping(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { Helper.booleanToFuture(failMsg = if (ApiRole.valueOf(entitlement.role_name).requiresBankId) EntitlementIsBankRole else EntitlementIsSystemRole, cc = callContext) { ApiRole.valueOf(entitlement.role_name).requiresBankId == entitlement.bank_id.nonEmpty } } - private def checkRolesBankId(callContext: Option[CallContext], postedData: PostCreateUserWithRolesJsonV400) = { - Future.sequence(postedData.roles.map(checkRoleBankIdRequirement(callContext,_))) + private def checkRoleBankIdMappings(callContext: Option[CallContext], postedData: PostCreateUserWithRolesJsonV400) = { + Future.sequence(postedData.roles.map(checkRoleBankIdMapping(callContext,_))) } private def checkRoleName(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { From d6887f38154dd1f64a645059bd90008f2d5a3833 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 16 Nov 2021 16:48:35 +0100 Subject: [PATCH 134/185] feature/remove the UserNotSuperAdmin for createUserWithRoles endpoint --- .../main/scala/code/api/v4_0_0/APIMethods400.scala | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 9126f803f..d5febdcad 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -2757,8 +2757,6 @@ trait APIMethods400 { entitlementsJsonV400, List( UserNotLoggedIn, - UserNotFoundById, - UserNotSuperAdmin, InvalidJsonFormat, IncorrectRoleName, EntitlementIsBankRole, @@ -2785,13 +2783,7 @@ trait APIMethods400 { _ <- checkRolesName(callContext, postedData) - allowedEntitlements = canCreateEntitlementAtAnyBank :: Nil - allowedEntitlementsTxt = UserNotSuperAdmin +" or" + UserHasMissingRoles + canCreateEntitlementAtAnyBank - _ <- - if(isSuperAdmin(loggedInUser.userId)) - Future.successful(Full(Unit)) - else - NewStyle.function.hasAtLeastOneEntitlement(allowedEntitlementsTxt)("", loggedInUser.userId, allowedEntitlements, callContext) + _ <- NewStyle.function.hasEntitlement("", loggedInUser.userId, canCreateEntitlementAtAnyBank, cc.callContext) (postBodyUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) @@ -4223,7 +4215,7 @@ trait APIMethods400 { } _ <- NewStyle.function.canGrantAccessToView(bankId, accountId, cc.loggedInUser, cc.callContext) (user, callContext) <- NewStyle.function.findByUserId(postJson.user_id, cc.callContext) - view <- getViews(bankId, accountId, postJson.view, callContext) + view <- getView(bankId, accountId, postJson.view, callContext) addedView <- createAccountAccessToUser(bankId, accountId, user, view, callContext) } yield { val viewJson = JSONFactory300.createViewJSON(addedView) From cbea7dff0f9ae8fa6e87ee14f546d5e9df0b6b1d Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 17 Nov 2021 12:31:25 +0100 Subject: [PATCH 135/185] feature/added the operationId guard for createMyApiCollectionEndpoint and createMyApiCollectionEndpointById --- obp-api/src/main/scala/code/api/util/ErrorMessages.scala | 1 + .../src/main/scala/code/api/v4_0_0/APIMethods400.scala | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 1d3dc1c11..ca6d8db0e 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -538,6 +538,7 @@ object ErrorMessages { val DynamicMessageDocNotFound = "OBP-40043: DynamicMessageDoc not found, please specify valid DYNAMIC_MESSAGE_DOC_ID. " val DynamicMessageDocDeleteError = "OBP-40044: DynamicMessageDoc can not be deleted. " val DynamicCodeCompileFail = "OBP-40045: The code to do compile is illegal scala code, compilation failed. " + val InvalidOperationId = "OBP-40046: Invalid operation_id, please specify valid operation_id." // Exceptions (OBP-50XXX) val UnknownError = "OBP-50000: Unknown Error." val FutureTimeoutException = "OBP-50001: Future Timeout Exception." diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index d5febdcad..a1461de30 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -8300,6 +8300,10 @@ trait APIMethods400 { postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostApiCollectionEndpointJson400", 400, cc.callContext) { json.extract[PostApiCollectionEndpointJson400] } + _ <- Helper.booleanToFuture(failMsg = s"$InvalidOperationId Current OPERATION_ID(${postJson.operation_id})", cc=Some(cc)) { + (DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc ++ DynamicEndpoints.dynamicResourceDocs ++ OBPAPI4_0_0.allResourceDocs).toList + .find(_.operationId==postJson.operation_id).isDefined + } (apiCollection, callContext) <- NewStyle.function.getApiCollectionByUserIdAndCollectionName(cc.userId, apiCollectionName, Some(cc)) apiCollectionEndpoint <- Future{MappedApiCollectionEndpointsProvider.getApiCollectionEndpointByApiCollectionIdAndOperationId(apiCollection.apiCollectionId, postJson.operation_id)} _ <- Helper.booleanToFuture(failMsg = s"$ApiCollectionEndpointAlreadyExisting Current OPERATION_ID(${postJson.operation_id}) is already in API_COLLECTION_NAME($apiCollectionName) ", cc=callContext) { @@ -8346,6 +8350,10 @@ trait APIMethods400 { postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostApiCollectionEndpointJson400", 400, cc.callContext) { json.extract[PostApiCollectionEndpointJson400] } + _ <- Helper.booleanToFuture(failMsg = s"$InvalidOperationId Current OPERATION_ID(${postJson.operation_id})", cc=Some(cc)) { + (DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc ++ DynamicEndpoints.dynamicResourceDocs ++ OBPAPI4_0_0.allResourceDocs).toList + .find(_.operationId==postJson.operation_id).isDefined + } (apiCollection, callContext) <- NewStyle.function.getApiCollectionById(apiCollectioId, Some(cc)) apiCollectionEndpoint <- Future{MappedApiCollectionEndpointsProvider.getApiCollectionEndpointByApiCollectionIdAndOperationId(apiCollection.apiCollectionId, postJson.operation_id)} _ <- Helper.booleanToFuture(failMsg = s"$ApiCollectionEndpointAlreadyExisting Current OPERATION_ID(${postJson.operation_id}) is already in API_COLLECTION_ID($apiCollectioId) ", cc=callContext) { From b06c9af9800440c43d50e4b2664971ad0c3fb98a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 17 Nov 2021 12:36:56 +0100 Subject: [PATCH 136/185] feature/Make RunMTLSWebApp out of the box feature --- obp-api/src/test/scala/RunMTLSWebApp.scala | 9 +++++++- .../code/setup/PropsProgrammatically.scala | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 obp-api/src/test/scala/code/setup/PropsProgrammatically.scala diff --git a/obp-api/src/test/scala/RunMTLSWebApp.scala b/obp-api/src/test/scala/RunMTLSWebApp.scala index 1743b8925..93a50ed58 100644 --- a/obp-api/src/test/scala/RunMTLSWebApp.scala +++ b/obp-api/src/test/scala/RunMTLSWebApp.scala @@ -31,6 +31,7 @@ import java.security.cert.X509Certificate import bootstrap.liftweb.Boot import code.api.RequestHeader import code.api.util.APIUtil +import code.setup.PropsProgrammatically import net.liftweb.http.LiftRules import net.liftweb.http.provider.HTTPContext import org.apache.commons.codec.binary.Base64 @@ -39,10 +40,16 @@ import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.webapp.WebAppContext import sun.security.provider.X509Factory -object RunMTLSWebApp extends App { +object RunMTLSWebApp extends App with PropsProgrammatically { val servletContextPath = "/" //set run mode value to "development", So the value is true of Props.devMode System.setProperty("run.mode", "development") + // Props hostname MUST be set to https protocol. + // Otherwise OAuth1.0a computed signature at OBP-API side cannot match API-Explorer generates + // This automatic adjustment should enable out-of-box feature + APIUtil.getPropsValue("hostname").map{ x => + setPropsValues("hostname"-> x.replaceFirst("http", "https")) + } { val tempHTTPContext = JProxy.newProxyInstance(this.getClass.getClassLoader, Array(classOf[HTTPContext]), diff --git a/obp-api/src/test/scala/code/setup/PropsProgrammatically.scala b/obp-api/src/test/scala/code/setup/PropsProgrammatically.scala new file mode 100644 index 000000000..6eb21025a --- /dev/null +++ b/obp-api/src/test/scala/code/setup/PropsProgrammatically.scala @@ -0,0 +1,23 @@ +package code.setup + +import net.liftweb.util.Props +import org.apache.commons.lang3.reflect.FieldUtils +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite} + + +/** + * Any unit test that extends this trait, have a chance to set new Props value, + * after each test rollback original Props values + */ +trait PropsProgrammatically { + + private def getLockedProviders = { + FieldUtils.readDeclaredField(Props, "net$liftweb$util$Props$$lockedProviders", true) + .asInstanceOf[List[Map[String, String]]] + } + + def setPropsValues(keyValues: (String, String)*): Unit = { + val newLockedProviders = keyValues.toMap :: getLockedProviders + FieldUtils.writeDeclaredField(Props, "net$liftweb$util$Props$$lockedProviders", newLockedProviders, true) + } +} From 1acacabc548b501a2916aa6a78129dab4d099994 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 17 Nov 2021 14:11:12 +0100 Subject: [PATCH 137/185] /feature: Added XBT to fallbackExchangeRates --- obp-api/src/main/scala/code/fx/fx.scala | 40 +++++++++++++++---------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/obp-api/src/main/scala/code/fx/fx.scala b/obp-api/src/main/scala/code/fx/fx.scala index 4db9fd784..e02156040 100644 --- a/obp-api/src/main/scala/code/fx/fx.scala +++ b/obp-api/src/main/scala/code/fx/fx.scala @@ -27,21 +27,32 @@ object fx extends MdcLoggable { // Make this easier //get data from : http://www.xe.com/de/currencyconverter/convert/?Amount=1&From=AUD&To=EUR - val fallbackExchangeRates = { + // Currently There are 14 currencies with 14 mappings etc. + // We don't actually need to store the Same:Same currency (1:1) but it makes editing the map less error prone in terms of data entry! + // So keep the Map balanced. + // If get compile error with type mismatch; + // found : AnyVal + // required: Double + // check the map is complete for all combinations - (We could use getOrElse (1.0) (double) as defaults in calling function but then we risk having missing values below) + // and make sure to sure explicit doubles e.g. 1.0 rather than 1 !! + + + val fallbackExchangeRates: Map[String, Map[String, Double]] = { Map( - "GBP" -> Map("EUR" -> 1.16278, "USD" -> 1.24930, "JPY" -> 141.373, "AED" -> 4.58882, "INR" -> 84.0950, "KRW" -> 1433.92, "XAF" -> 762.826, "JOD" -> 0.936707, "ILS" -> 4.70020, "AUD" -> 1.63992 ,"HKD" -> 10.1468, "MXN" -> 29.2420), - "EUR" -> Map("GBP" -> 0.860011, "USD" -> 1.07428, "JPY" -> 121.567, "AED" -> 3.94594, "INR" -> 72.3136, "KRW" -> 1233.03, "XAF" -> 655.957, "JOD" -> 0.838098, "ILS" -> 4.20494, "AUD" -> 1.49707 ,"HKD" -> 8.88926, "MXN" -> 26.0359), - "USD" -> Map("GBP" -> 0.800446, "EUR" -> 0.930886, "JPY" -> 113.161, "AED" -> 3.67310, "INR" -> 67.3135, "KRW" -> 1147.78, "XAF" -> 610.601, "JOD" -> 0.708659, "ILS" -> 3.55495, "AUD" -> 1.27347 ,"HKD" -> 7.84766, "MXN" -> 21.7480), - "JPY" -> Map("GBP" -> 0.00707350, "EUR" -> 0.00822592, "USD" -> 0.00883695, "AED" -> 0.0324590, "INR" -> 0.594846, "KRW" -> 10.1428, "XAF" -> 5.39585, "JOD" -> 0.00639777, "ILS" -> 0.0320926, "AUD" -> 0.0114819 ,"HKD" -> 0.0709891, "MXN" -> 0.2053), - "AED" -> Map("GBP" -> 0.217921, "EUR" -> 0.253425, "USD" -> 0.272250, "JPY" -> 30.8081, "INR" -> 18.3255, "KRW" -> 312.482, "XAF" -> 166.236, "AED" -> 0.192964, "ILS" -> 0.968033, "AUD" -> 0.346779 ,"HKD" -> 2.13685, "MXN" -> 5.9217), - "INR" -> Map("GBP" -> 0.0118913, "EUR" -> 0.0138287, "USD" -> 0.0148559, "JPY" -> 1.68111, "AED" -> 0.0545671, "KRW" -> 17.0512, "XAF" -> 9.07101, "JOD" -> 0.0110959 , "ILS" -> 0.0556764, "AUD" -> 0.0198319 ,"HKD" -> 0.109972, "MXN" -> 0.2983), - "KRW" -> Map("GBP" -> 0.000697389, "EUR" -> 0.000811008, "USD" -> 0.000871250, "JPY" -> 0.0985917, "AED" -> 0.00320019, "INR" -> 0.0586469, "XAF" -> 0.531986, "JOD" -> 0.000630634, "ILS" -> 0.00316552,"AUD" -> 0.00111694,"HKD" -> 0.00697233, "MXN" -> 0.0183), - "XAF" -> Map("GBP" -> 0.00131092, "EUR" -> 0.00152449, "USD" -> 0.00163773, "JPY" -> 0.185328, "AED" -> 0.00601555, "INR" -> 0.110241, "KRW" -> 1.87975, "JOD" -> 0.00127784, "ILS" -> 0.00641333,"AUD" -> 0.00228226,"HKD" -> 0.0135503, "MXN" -> 0.0396 ), - "JOD" -> Map("GBP" -> 1.06757, "EUR" -> 0.237707, "USD" -> 1.41112, "JPY" -> 156.304, "AED" -> 5.18231, "INR" -> 90.1236, "KRW" -> 1585.68, "XAF" -> 782.572, "ILS" -> 5.02018, "AUD" -> 1.63992 ,"HKD" -> 11.0687, "MXN" -> 30.8336), - "ILS" -> Map("GBP" -> 0.212763, "EUR" -> 1.19318, "USD" -> 0.281298, "JPY" -> 31.1599, "AED" -> 1.03302, "INR" -> 17.9609, "KRW" -> 315.903, "XAF" -> 155.925, "JOD" -> 0.199196, "AUD" -> 0.352661 ,"HKD" -> 2.16985, "MXN" -> 6.4871), - "AUD" -> Map("GBP" -> 0.609788, "EUR" -> 0.667969, "USD" -> 0.785256, "JPY" -> 87.0936, "AED" -> 2.88368, "INR" -> 50.4238, "KRW" -> 895.304, "XAF" -> 438.162, "JOD" -> 0.556152, "ILS" -> 2.83558 ,"HKD" -> 5.61346 , "MXN" -> 16.0826), - "HKD" -> Map("GBP" -> 0.0985443, "EUR" -> 0.112495, "USD" -> 0.127427, "JPY" -> 14.0867, "AED" -> 0.467977, "INR" -> 9.09325, "KRW" -> 143.424, "XAF" -> 73.8049, "JOD" -> 0.0903452, "ILS" -> 0.460862 ,"AUD" -> 0.178137, "MXN" -> 2.8067), - "MXN" -> Map("GBP" -> 0.0341, "EUR" -> 0.0384, "USD" -> 0.0459, "JPY" -> 4.8687, "AED" -> 0.1688, "INR" -> 3.3513, "KRW" -> 54.4512, "XAF" -> 25.1890, "JOD" -> 0.0324, "ILS" -> 0.1541 , "AUD" -> 0.0621, "HKD" -> 0.3562 ) + "GBP" -> Map("GBP" -> 1.0, "EUR" -> 1.16278, "USD" -> 1.24930, "JPY" -> 141.373, "AED" -> 4.58882, "INR" -> 84.0950, "KRW" -> 1433.92, "XAF" -> 762.826, "JOD" -> 0.936707, "ILS" -> 4.70020, "AUD" -> 1.63992, "HKD" -> 10.1468, "MXN" -> 29.2420, "XBT" -> 0.000022756409956), + "EUR" -> Map("GBP" -> 0.860011, "EUR" -> 1.0, "USD" -> 1.07428, "JPY" -> 121.567, "AED" -> 3.94594, "INR" -> 72.3136, "KRW" -> 1233.03, "XAF" -> 655.957, "JOD" -> 0.838098, "ILS" -> 4.20494, "AUD" -> 1.49707, "HKD" -> 8.88926, "MXN" -> 26.0359, "XBT" -> 0.000019087905636), + "USD" -> Map("GBP" -> 0.800446, "EUR" -> 0.930886, "USD" -> 1.0, "JPY" -> 113.161, "AED" -> 3.67310, "INR" -> 67.3135, "KRW" -> 1147.78, "XAF" -> 610.601, "JOD" -> 0.708659, "ILS" -> 3.55495, "AUD" -> 1.27347, "HKD" -> 7.84766, "MXN" -> 21.7480, "XBT" -> 0.0000169154), + "JPY" -> Map("GBP" -> 0.00707350, "EUR" -> 0.00822592, "USD" -> 0.00883695, "JPY" -> 1.0, "AED" -> 0.0324590, "INR" -> 0.594846, "KRW" -> 10.1428, "XAF" -> 5.39585, "JOD" -> 0.00639777, "ILS" -> 0.0320926, "AUD" -> 0.0114819, "HKD" -> 0.0709891, "MXN" -> 0.2053, "XBT" -> 0.000000147171931), + "AED" -> Map("GBP" -> 0.217921, "EUR" -> 0.253425, "USD" -> 0.272250, "JPY" -> 30.8081, "AED" -> 1.0, "INR" -> 18.3255, "KRW" -> 312.482, "XAF" -> 166.236, "JOD" -> 0.1930565, "ILS" -> 0.968033, "AUD" -> 0.346779, "HKD" -> 2.13685, "MXN" -> 5.9217, "XBT" -> 0.000004603349217), + "INR" -> Map("GBP" -> 0.0118913, "EUR" -> 0.0138287, "USD" -> 0.0148559, "JPY" -> 1.68111, "AED" -> 0.0545671, "INR" -> 1.0, "KRW" -> 17.0512, "XAF" -> 9.07101, "JOD" -> 0.0110959, "ILS" -> 0.0556764, "AUD" -> 0.0198319, "HKD" -> 0.109972, "MXN" -> 0.2983, "XBT" -> 0.00000022689396), + "KRW" -> Map("GBP" -> 0.000697389, "EUR" -> 0.000811008, "USD" -> 0.000871250, "JPY" -> 0.0985917, "AED" -> 0.00320019, "INR" -> 0.0586469, "KRW" -> 1.0, "XAF" -> 0.531986, "JOD" -> 0.000630634, "ILS" -> 0.00316552, "AUD" -> 0.00111694,"HKD" -> 0.00697233,"MXN" -> 0.0183, "XBT" -> 0.000000014234725), + "XAF" -> Map("GBP" -> 0.00131092, "EUR" -> 0.00152449, "USD" -> 0.00163773, "JPY" -> 0.185328, "AED" -> 0.00601555, "INR" -> 0.110241, "KRW" -> 1.87975, "XAF" -> 1.0, "JOD" -> 0.00127784, "ILS" -> 0.00641333, "AUD" -> 0.00228226,"HKD" -> 0.0135503, "MXN" -> 0.0396, "XBT" -> 0.000000029074795), + "JOD" -> Map("GBP" -> 1.06757, "EUR" -> 0.237707, "USD" -> 1.41112, "JPY" -> 156.304, "AED" -> 5.18231, "INR" -> 90.1236, "KRW" -> 1585.68, "XAF" -> 782.572, "JOD" -> 1.0, "ILS" -> 5.02018, "AUD" -> 1.63992, "HKD" -> 11.0687, "MXN" -> 30.8336, "XBT" -> 0.000023803244006), + "ILS" -> Map("GBP" -> 0.212763, "EUR" -> 1.19318, "USD" -> 0.281298, "JPY" -> 31.1599, "AED" -> 1.03302, "INR" -> 17.9609, "KRW" -> 315.903, "XAF" -> 155.925, "JOD" -> 0.199196, "ILS" -> 1.0, "AUD" -> 0.352661, "HKD" -> 2.16985, "MXN" -> 6.4871, "XBT" -> 0.000005452272147), + "AUD" -> Map("GBP" -> 0.609788, "EUR" -> 0.667969, "USD" -> 0.785256, "JPY" -> 87.0936, "AED" -> 2.88368, "INR" -> 50.4238, "KRW" -> 895.304, "XAF" -> 438.162, "JOD" -> 0.556152, "ILS" -> 2.83558, "AUD" -> 1.0, "HKD" -> 5.61346, "MXN" -> 16.0826, "XBT" -> 0.000012284055924), + "HKD" -> Map("GBP" -> 0.0985443, "EUR" -> 0.112495, "USD" -> 0.127427, "JPY" -> 14.0867, "AED" -> 0.467977, "INR" -> 9.09325, "KRW" -> 143.424, "XAF" -> 73.8049, "JOD" -> 0.0903452, "ILS" -> 0.460862, "AUD" -> 0.178137, "HKD" -> 1.0, "MXN" -> 2.8067, "XBT" -> 0.000002164242461), + "MXN" -> Map("GBP" -> 0.0341, "EUR" -> 0.0384, "USD" -> 0.0459, "JPY" -> 4.8687, "AED" -> 0.1688, "INR" -> 3.3513, "KRW" -> 54.4512, "XAF" -> 25.1890, "JOD" -> 0.0324, "ILS" -> 0.1541, "AUD" -> 0.0621, "HKD" -> 0.3562, "MXN" -> 1.0, "XBT" -> 0.00000081112586), + "XBT" -> Map("GBP" -> 44188.118, "EUR" -> 52436.431, "USD" -> 59245.918, "JPY" -> 6805170.8, "AED" -> 217414.47, "INR" -> 4407607.74,"KRW" -> 70131575, "XAF" -> 34353824, "JOD" -> 41960.111, "ILS" -> 182981.21, "AUD" -> 81168.603, "HKD" -> 460448.90, "MXN" -> 1230503.30,"XBT" -> 1.0) ) } @@ -104,7 +115,6 @@ object fx extends MdcLoggable { } else { //logger.debug(s"fromAmount is $fromAmount, toCurrency is ${toCurrency}") val rate: Option[Double] = try { - // Get the translated name out of the map Some(fallbackExchangeRates.get(fromCurrency).get(toCurrency)) } catch { From 6836db5fd7300ac42acb4cf64194a88908b8a08b Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Wed, 17 Nov 2021 16:16:11 +0100 Subject: [PATCH 138/185] /feature Adding XBT to isValidCurrencyISOCode --- obp-api/src/main/scala/code/api/util/APIUtil.scala | 3 ++- obp-api/src/main/scala/code/fx/fx.scala | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index e280a306f..971332fb3 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -635,7 +635,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ /** check the currency ISO code from the ISOCurrencyCodes.xml file */ def isValidCurrencyISOCode(currencyCode: String): Boolean = { - val currencyIsoCodeArray = (CurrencyIsoCodeFromXmlFile \"CcyTbl" \ "CcyNtry" \ "Ccy").map(_.text).mkString(" ").split("\\s+") + // Note: We add BTC bitcoin as XBT (the ISO compliant varient) + val currencyIsoCodeArray = (CurrencyIsoCodeFromXmlFile \"CcyTbl" \ "CcyNtry" \ "Ccy").map(_.text).mkString(" ").split("\\s+") :+ "XBT" currencyIsoCodeArray.contains(currencyCode) } diff --git a/obp-api/src/main/scala/code/fx/fx.scala b/obp-api/src/main/scala/code/fx/fx.scala index e02156040..d5935bdfd 100644 --- a/obp-api/src/main/scala/code/fx/fx.scala +++ b/obp-api/src/main/scala/code/fx/fx.scala @@ -36,6 +36,7 @@ object fx extends MdcLoggable { // check the map is complete for all combinations - (We could use getOrElse (1.0) (double) as defaults in calling function but then we risk having missing values below) // and make sure to sure explicit doubles e.g. 1.0 rather than 1 !! + // Note: If you add a non ISO standard currency below, you will also need to add it also to isValidCurrencyISOCode otherwise FX endpoints etc will fail. val fallbackExchangeRates: Map[String, Map[String, Double]] = { Map( From b96db8dcf30812f0d0ebbb4baae316a72deeabf9 Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 17 Nov 2021 16:51:26 +0100 Subject: [PATCH 139/185] feature/OBPv400 added deleteMyApiCollectionEndpointByOperationId --- .../scala/code/api/v4_0_0/APIMethods400.scala | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index a1461de30..41ef8461a 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -2752,7 +2752,7 @@ trait APIMethods400 { | |For a Bank level Role (e.g. CanCreateAccount), set bank_id to a valid value e.g. "bank_id":"my-bank-id" | - |Authentication is required and the user needs to be a Super Admin. Super Admins are listed in the Props file.""", + |""", postCreateUserWithRolesJsonV400, entitlementsJsonV400, List( @@ -8542,16 +8542,53 @@ trait APIMethods400 { } } + staticResourceDocs += ResourceDoc( + deleteMyApiCollectionEndpointByOperationId, + implementedInApiVersion, + nameOf(deleteMyApiCollectionEndpointByOperationId), + "DELETE", + "/my/api-collection-ids/API_COLLECTION_ID/api-collection-endpoints/OPERATION_ID", + "Delete My Api Collection Endpoint By Id", + s"""${Glossary.getGlossaryItem("API Collections")} + | + |Delete Api Collection Endpoint By OPERATION_ID + | + |${authenticationRequiredMessage(true)} + | + |""", + EmptyBody, + Full(true), + List( + $UserNotLoggedIn, + UserNotFoundByUserId, + UnknownError + ), + List(apiTagApiCollection, apiTagNewStyle) + ) + + lazy val deleteMyApiCollectionEndpointByOperationId : OBPEndpoint = { + case "my" :: "api-collection-ids" :: apiCollectionId :: "api-collection-endpoints" :: operationId :: Nil JsonDelete _ => { + cc => + for { + (apiCollection, callContext) <- NewStyle.function.getApiCollectionById(apiCollectionId, Some(cc) ) + (apiCollectionEndpoint, callContext) <- NewStyle.function.getApiCollectionEndpointByApiCollectionIdAndOperationId(apiCollection.apiCollectionId, operationId, callContext) + (deleted, callContext) <- NewStyle.function.deleteApiCollectionEndpointById(apiCollectionEndpoint.apiCollectionEndpointId, callContext) + } yield { + (Full(deleted), HttpCode.`204`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( deleteMyApiCollectionEndpointById, implementedInApiVersion, nameOf(deleteMyApiCollectionEndpointById), "DELETE", - "/my/api-collections-ids/API_COLLECTION_ID/api-collection-endpoints/OPERATION_ID", + "/my/api-collection-ids/API_COLLECTION_ID/api-collection-endpoint-ids/API_COLLECTION_ENDPOINT_ID", "Delete My Api Collection Endpoint By Id", s"""${Glossary.getGlossaryItem("API Collections")} - | - |Delete Api Collection Endpoint By Id + |Delete Api Collection Endpoint + |Delete Api Collection Endpoint By IdDelete Api Collection Endpoint | |${authenticationRequiredMessage(true)} | @@ -8567,18 +8604,18 @@ trait APIMethods400 { ) lazy val deleteMyApiCollectionEndpointById : OBPEndpoint = { - case "my" :: "api-collections-ids" :: apiCollectionId :: "api-collection-endpoints" :: operationId :: Nil JsonDelete _ => { + case "my" :: "api-collection-ids" :: apiCollectionId :: "api-collection-endpoint-ids" :: apiCollectionEndpointId :: Nil JsonDelete _ => { cc => for { (apiCollection, callContext) <- NewStyle.function.getApiCollectionById(apiCollectionId, Some(cc) ) - (apiCollectionEndpoint, callContext) <- NewStyle.function.getApiCollectionEndpointByApiCollectionIdAndOperationId(apiCollection.apiCollectionId, operationId, callContext) + (apiCollectionEndpoint, callContext) <- NewStyle.function.getApiCollectionEndpointById(apiCollectionEndpointId, callContext) (deleted, callContext) <- NewStyle.function.deleteApiCollectionEndpointById(apiCollectionEndpoint.apiCollectionEndpointId, callContext) } yield { (Full(deleted), HttpCode.`204`(callContext)) } } } - + staticResourceDocs += ResourceDoc( createJsonSchemaValidation, implementedInApiVersion, From f55107373e16771522426c0890318eb506e81c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 18 Nov 2021 10:13:13 +0100 Subject: [PATCH 140/185] refactor/Factor out commo code at function getUserAndSessionContextFuture --- .../main/scala/code/api/util/APIUtil.scala | 112 ++++-------------- .../main/scala/code/api/util/ApiAuth.scala | 90 ++++++++++++++ 2 files changed, 112 insertions(+), 90 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/util/ApiAuth.scala diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index e280a306f..91adeda00 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -45,35 +45,37 @@ import code.api.util.APIUtil.ResourceDoc.{findPathVariableNames, isPathVariable} import code.api.util.ApiRole.{canCreateProduct, canCreateProductAtAnyBank} import code.api.util.ApiTag.{ResourceDocTag, apiTagBank, apiTagNewStyle} import code.api.util.Glossary.GlossaryItem -import code.api.util.JwsUtil.getJwsHeaderValue -import code.api.util.RateLimitingJson.CallLimit import code.api.v1_2.ErrorMessage import code.api.v2_0_0.CreateEntitlementJSON -import code.api.{DirectLogin, _} import code.api.v4_0_0.dynamic.{DynamicEndpointHelper, DynamicEndpoints, DynamicEntityHelper} +import code.api.{DirectLogin, _} import code.authtypevalidation.AuthenticationTypeValidationProvider import code.bankconnectors.Connector import code.consumer.Consumers import code.customer.CustomerX import code.entitlement.Entitlement -import code.loginattempts.LoginAttempt import code.metrics._ import code.model._ import code.model.dataAccess.AuthUser -import code.ratelimiting.{RateLimiting, RateLimitingDI} import code.sanitycheck.SanityCheck import code.scope.Scope import code.usercustomerlinks.UserCustomerLink -import code.util.{Helper, JsonSchemaUtil} import code.util.Helper.{MdcLoggable, SILENCE_IS_GOLDEN} +import code.util.{Helper, JsonSchemaUtil} import code.views.Views import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue import com.alibaba.ttl.internal.javassist.CannotCompileException import com.github.dwickern.macros.NameOf.{nameOf, nameOfType} +import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.enums.StrongCustomerAuthentication.SCA import com.openbankproject.commons.model.enums.{PemCertificateRole, StrongCustomerAuthentication} import com.openbankproject.commons.model.{Customer, _} +import com.openbankproject.commons.util.Functions.Implicits._ +import com.openbankproject.commons.util.Functions.Memo +import com.openbankproject.commons.util._ import dispatch.url +import javassist.expr.{ExprEditor, MethodCall} +import javassist.{ClassPool, LoaderClassPath} import net.liftweb.actor.LAFuture import net.liftweb.common.{Empty, _} import net.liftweb.http._ @@ -85,21 +87,14 @@ import net.liftweb.json.JsonAST.{JField, JNothing, JObject, JString, JValue} import net.liftweb.json.JsonParser.ParseException import net.liftweb.json._ import net.liftweb.util.Helpers._ -import net.liftweb.util.{Helpers, LiftFlowOfControlException, Props, StringHelpers, ThreadGlobal} - -import scala.collection.JavaConverters._ -import scala.collection.immutable.{List, Nil} -import scala.collection.mutable.{ArrayBuffer, ListBuffer} -import com.openbankproject.commons.ExecutionContext.Implicits.global -import com.openbankproject.commons.util.{ApiVersion, Functions, JsonAble, ReflectUtils, ScannedApiVersion} -import com.openbankproject.commons.util.Functions.Implicits._ -import com.openbankproject.commons.util.Functions.Memo -import javassist.{ClassPool, LoaderClassPath} -import javassist.expr.{ExprEditor, MethodCall} +import net.liftweb.util._ import org.apache.commons.io.IOUtils import org.apache.commons.lang3.StringUtils +import scala.collection.JavaConverters._ +import scala.collection.immutable.{List, Nil} import scala.collection.mutable +import scala.collection.mutable.{ArrayBuffer, ListBuffer} import scala.concurrent.Future import scala.io.BufferedSource import scala.util.Either @@ -2767,80 +2762,15 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ Future { (Empty, Some(cc)) } } + // COMMON POST AUTHENTICATION CODE GOES BELOW + // Check is it a user deleted or locked - val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = for { - (user: Box[User], cc) <- res - } yield { - user match { - case Full(u) => // There is a user. Check it. - if(u.isDeleted.getOrElse(false)) { - (Failure(UserIsDeleted) , cc) // The user is DELETED. - } else { - LoginAttempt.userIsLocked(u.name) match { - case true => (Failure(UsernameHasBeenLocked),cc) // The user is LOCKED. - case false => (user, cc) // All good - } - } - case _ => // There is no user. Just forward the result. - (user, cc) - } - } + val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = ApiAuth.checkUserIsDeletedOrLocked(res) + // Check Rate Limiting + val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = ApiAuth.checkRateLimiting(userIsLockedOrDeleted) - - /****************************************************************************************************************** - * This block of code needs to update Call Context with Rate Limiting - * Please note that first source is the table RateLimiting and second is the table Consumer - */ - def getRateLimiting(consumerId: String, version: String, name: String): Future[Box[RateLimiting]] = { - RateLimitingUtil.useConsumerLimits match { - case true => RateLimitingDI.rateLimiting.vend.getByConsumerId(consumerId, version, name, Some(new Date())) - case false => Future(Empty) - } - } - val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = for { - (user, cc) <- userIsLockedOrDeleted - consumer = cc.flatMap(_.consumer) - version = cc.map(_.implementedInVersion).getOrElse("None") // Calculate apiVersion in case of Rate Limiting - operationId = cc.flatMap(_.operationId) // Unique Identifier of Dynamic Endpoints - // Calculate apiName in case of Rate Limiting - name = cc.flatMap(_.resourceDocument.map(_.partialFunctionName)) // 1st try: function name at resource doc - .orElse(operationId) // 2nd try: In case of Dynamic Endpoint we can only use operationId - .getOrElse("None") // Not found any unique identifier - rateLimiting <- getRateLimiting(consumer.map(_.consumerId.get).getOrElse(""), version, name) - } yield { - val limit: Option[CallLimit] = rateLimiting match { - case Full(rl) => Some(CallLimit( - rl.consumerId, - rl.apiName, - rl.apiVersion, - rl.bankId, - rl.perSecondCallLimit, - rl.perMinuteCallLimit, - rl.perHourCallLimit, - rl.perDayCallLimit, - rl.perWeekCallLimit, - rl.perMonthCallLimit)) - case Empty => - Some(CallLimit( - consumer.map(_.consumerId.get).getOrElse(""), - None, - None, - None, - consumer.map(_.perSecondCallLimit.get).getOrElse(-1), - consumer.map(_.perMinuteCallLimit.get).getOrElse(-1), - consumer.map(_.perHourCallLimit.get).getOrElse(-1), - consumer.map(_.perDayCallLimit.get).getOrElse(-1), - consumer.map(_.perWeekCallLimit.get).getOrElse(-1), - consumer.map(_.perMonthCallLimit.get).getOrElse(-1) - )) - case _ => None - } - (user, cc.map(_.copy(rateLimiting = limit))) - } - /*************************************************************************************************************** */ - - - resultWithRateLimiting map { // Update Call Context + // Update Call Context + resultWithRateLimiting map { x => (x._1, ApiSession.updateCallContext(Spelling(spelling), x._2)) } map { x => (x._1, x._2.map(_.copy(implementedInVersion = implementedInVersion))) @@ -2861,7 +2791,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } } - + + + /** * This Function is used to terminate a Future used in for-comprehension with specific message and code in case that value of Box is not Full. diff --git a/obp-api/src/main/scala/code/api/util/ApiAuth.scala b/obp-api/src/main/scala/code/api/util/ApiAuth.scala new file mode 100644 index 000000000..5acc2bca7 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/ApiAuth.scala @@ -0,0 +1,90 @@ +package code.api.util + +import java.util.Date + +import code.api.util.ErrorMessages.{UserIsDeleted, UsernameHasBeenLocked} +import code.api.util.RateLimitingJson.CallLimit +import code.loginattempts.LoginAttempt +import code.ratelimiting.{RateLimiting, RateLimitingDI} +import com.openbankproject.commons.model.User +import net.liftweb.common.{Box, Empty, Failure, Full} +import com.openbankproject.commons.ExecutionContext.Implicits.global + +import scala.concurrent.Future + + +object ApiAuth { + def checkUserIsDeletedOrLocked(res: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = { + for { + (user: Box[User], cc) <- res + } yield { + user match { + case Full(u) => // There is a user. Check it. + if (u.isDeleted.getOrElse(false)) { + (Failure(UserIsDeleted), cc) // The user is DELETED. + } else { + LoginAttempt.userIsLocked(u.name) match { + case true => (Failure(UsernameHasBeenLocked), cc) // The user is LOCKED. + case false => (user, cc) // All good + } + } + case _ => // There is no user. Just forward the result. + (user, cc) + } + } + } + + /** + * This block of code needs to update Call Context with Rate Limiting + * Please note that first source is the table RateLimiting and second is the table Consumer + */ + def checkRateLimiting(userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = { + def getRateLimiting(consumerId: String, version: String, name: String): Future[Box[RateLimiting]] = { + RateLimitingUtil.useConsumerLimits match { + case true => RateLimitingDI.rateLimiting.vend.getByConsumerId(consumerId, version, name, Some(new Date())) + case false => Future(Empty) + } + } + for { + (user, cc) <- userIsLockedOrDeleted + consumer = cc.flatMap(_.consumer) + version = cc.map(_.implementedInVersion).getOrElse("None") // Calculate apiVersion in case of Rate Limiting + operationId = cc.flatMap(_.operationId) // Unique Identifier of Dynamic Endpoints + // Calculate apiName in case of Rate Limiting + name = cc.flatMap(_.resourceDocument.map(_.partialFunctionName)) // 1st try: function name at resource doc + .orElse(operationId) // 2nd try: In case of Dynamic Endpoint we can only use operationId + .getOrElse("None") // Not found any unique identifier + rateLimiting <- getRateLimiting(consumer.map(_.consumerId.get).getOrElse(""), version, name) + } yield { + val limit: Option[CallLimit] = rateLimiting match { + case Full(rl) => Some(CallLimit( + rl.consumerId, + rl.apiName, + rl.apiVersion, + rl.bankId, + rl.perSecondCallLimit, + rl.perMinuteCallLimit, + rl.perHourCallLimit, + rl.perDayCallLimit, + rl.perWeekCallLimit, + rl.perMonthCallLimit)) + case Empty => + Some(CallLimit( + consumer.map(_.consumerId.get).getOrElse(""), + None, + None, + None, + consumer.map(_.perSecondCallLimit.get).getOrElse(-1), + consumer.map(_.perMinuteCallLimit.get).getOrElse(-1), + consumer.map(_.perHourCallLimit.get).getOrElse(-1), + consumer.map(_.perDayCallLimit.get).getOrElse(-1), + consumer.map(_.perWeekCallLimit.get).getOrElse(-1), + consumer.map(_.perMonthCallLimit.get).getOrElse(-1) + )) + case _ => None + } + (user, cc.map(_.copy(rateLimiting = limit))) + } + } + +} From e02207069617b818c699f46460f3c4948f00f0e4 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 18 Nov 2021 11:49:07 +0100 Subject: [PATCH 141/185] feature/added the guard for createUserWithAccountAccess endpoint --- .../SwaggerDefinitionsJSON.scala | 2 +- .../scala/code/api/util/ErrorMessages.scala | 1 + .../scala/code/api/v4_0_0/APIMethods400.scala | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index ec030e79e..2712cd229 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4095,7 +4095,7 @@ object SwaggerDefinitionsJSON { ) val postCreateUserWithRolesJsonV400 = PostCreateUserWithRolesJsonV400( userIdExample.value, - providerExample.value, + s"dauth.${providerExample.value}", List(createEntitlementJSON) ) val revokedJsonV400 = RevokedJsonV400(true) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index ca6d8db0e..f1c5d1734 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -193,6 +193,7 @@ object ErrorMessages { val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " val CannotGetOrCreateUser = "OBP-20102: Cannot get or create user." + val InvalidUserProvider = "OBP-20103: Invalid User Provider." // OAuth 2 val ApplicationNotIdentified = "OBP-20200: The application cannot be identified. " diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 41ef8461a..9bd243779 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -2778,11 +2778,11 @@ trait APIMethods400 { } //check the system role bankId is Empty, but bank level role need bankId _ <- checkRoleBankIdMappings(callContext, postedData) - + _ <- checkRolesBankIdExsiting(callContext, postedData) - + _ <- checkRolesName(callContext, postedData) - + _ <- NewStyle.function.hasEntitlement("", loggedInUser.userId, canCreateEntitlementAtAnyBank, cc.callContext) (postBodyUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) @@ -2790,7 +2790,7 @@ trait APIMethods400 { _ <- checkIfUserAlreadyHasEntitlements(postedData, callContext) addedEntitlements <- addEntitlementsToUser(postedData, callContext) - + } yield { (JSONFactory400.createEntitlementJSONs(addedEntitlements), HttpCode.`201`(callContext)) } @@ -4257,6 +4257,16 @@ trait APIMethods400 { postJson <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { json.extract[PostCreateUserAccountAccessJsonV400] } + //provider must start with dauth., can not create other provider users. + _ <- Helper.booleanToFuture(s"$InvalidUserProvider The user.provider must be start with 'dauth.'", cc=Some(cc)) { + postJson.provider.startsWith("dauth.") + } + + //user_id set the length for the min length of the userId. eg: 36 + _ <- Helper.booleanToFuture(s"$InvalidUserId The user.user_id length must be at least 36. ", cc=Some(cc)) { + postJson.user_id.length>=36 + } + _ <- NewStyle.function.canGrantAccessToView(bankId, accountId, cc.loggedInUser, cc.callContext) (user, callContext) <- NewStyle.function.getOrCreateUser(postJson.user_id, postJson.provider, cc.callContext) views <- getViews(bankId, accountId, postJson, callContext) From 374acceede67303f7a63520d7a2db8bf686fe6f4 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 19 Nov 2021 00:07:46 +0100 Subject: [PATCH 142/185] feature/OBPv400 add guard for createUserWithRoles --- .../scala/code/api/util/ErrorMessages.scala | 3 +- .../scala/code/api/v4_0_0/APIMethods400.scala | 72 ++++++++++++++----- .../code/api/v4_0_0/AccountAccessTest.scala | 2 +- .../code/api/v4_0_0/EntitlementTests.scala | 56 +++++++++++++++ 4 files changed, 113 insertions(+), 20 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index f1c5d1734..9a8b5dcaf 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -193,7 +193,7 @@ object ErrorMessages { val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " val CannotGetOrCreateUser = "OBP-20102: Cannot get or create user." - val InvalidUserProvider = "OBP-20103: Invalid User Provider." + val InvalidUserProvider = "OBP-20103: Invalid DAuth User Provider." // OAuth 2 val ApplicationNotIdentified = "OBP-20200: The application cannot be identified. " @@ -412,6 +412,7 @@ object ErrorMessages { val EntitlementRequestNotFound = "OBP-30215: EntitlementRequestId not found" val EntitlementAlreadyExists = "OBP-30216: Entitlement already exists for the user." val EntitlementCannotBeDeleted = "OBP-30219: EntitlementId cannot be deleted." + val EntitlementCannotBeGranted = "OBP-30220: Entitlement cannot be granted." val CreateSystemViewError = "OBP-30250: Could not create the system view" val DeleteSystemViewError = "OBP-30251: Could not delete the system view" diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 9bd243779..865aa49e3 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -5,7 +5,7 @@ import code.DynamicEndpoint.DynamicEndpointSwagger import code.accountattribute.AccountAttributeX import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil.{fullBoxOrException, _} -import code.api.util.ApiRole._ +import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, _} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.ExampleValue._ @@ -2762,10 +2762,10 @@ trait APIMethods400 { EntitlementIsBankRole, EntitlementIsSystemRole, EntitlementAlreadyExists, + InvalidUserProvider, UnknownError ), - List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle), - Some(List(canCreateEntitlementAtOneBank,canCreateEntitlementAtAnyBank))) + List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle)) lazy val createUserWithRoles: OBPEndpoint = { case "user-entitlements" :: Nil JsonPost json -> _ => { @@ -2776,6 +2776,17 @@ trait APIMethods400 { postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { json.extract[PostCreateUserWithRolesJsonV400] } + + //provider must start with dauth., can not create other provider users. + _ <- Helper.booleanToFuture(s"$InvalidUserProvider The user.provider must be start with 'dauth.'", cc=Some(cc)) { + postedData.provider.startsWith("dauth.") + } + + //user_id set the length for the min length of the userId. eg: 36 + _ <- Helper.booleanToFuture(s"$InvalidUserId The user.user_id length must be at least 36. ", cc=Some(cc)) { + postedData.user_id.length>=36 + } + //check the system role bankId is Empty, but bank level role need bankId _ <- checkRoleBankIdMappings(callContext, postedData) @@ -2783,14 +2794,17 @@ trait APIMethods400 { _ <- checkRolesName(callContext, postedData) - _ <- NewStyle.function.hasEntitlement("", loggedInUser.userId, canCreateEntitlementAtAnyBank, cc.callContext) + canCreateEntitlementAtAnyBankRole = Entitlement.entitlement.vend.getEntitlement("", loggedInUser.userId, canCreateEntitlementAtAnyBank.toString()) - (postBodyUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) - - _ <- checkIfUserAlreadyHasEntitlements(postedData, callContext) + (requestUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) + + _ <- if (canCreateEntitlementAtAnyBankRole.isDefined) + checkRequestRolesForTheUser(requestUser.userId, postedData.roles, true, callContext) + else + checkRequestRolesForTheUser(loggedInUser.userId, postedData.roles, false, callContext) addedEntitlements <- addEntitlementsToUser(postedData, callContext) - + } yield { (JSONFactory400.createEntitlementJSONs(addedEntitlements), HttpCode.`201`(callContext)) } @@ -10909,17 +10923,39 @@ trait APIMethods400 { } private def addEntitlementsToUser(postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { - Future.sequence(postedData.roles.map(addEntitlementToUser(postedData.user_id, _, callContext))) + Future.sequence(postedData.roles.distinct.map(addEntitlementToUser(postedData.user_id, _, callContext))) } - - private def checkIfUserAlreadyHasEntitlement(userId:String, entitlement: CreateEntitlementJSON, callContext: Option[CallContext]) = { - Helper.booleanToFuture(failMsg = s"$EntitlementAlreadyExists Current Entitlement (${entitlement.role_name})", cc=callContext) { - hasEntitlement(entitlement.bank_id, userId, valueOf(entitlement.role_name)) == false - } - } - - private def checkIfUserAlreadyHasEntitlements(postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { - Future.sequence(postedData.roles.map(checkIfUserAlreadyHasEntitlement(postedData.user_id, _, callContext))) + + /** + * this method checks the roles for the userId, + * If isDuplicate = true, (mean the login user has canCreateEntitlementAtAnyBankRole), the userId = requestUserId, + * Here we will grant all the roles to the requestUserId, so first we need to check if the requestUserId already has the roles or not, so + * It will find the duplication ones between requestEntitlements and the userId's already has entitlements.--> throw the duplication error + * If isDuplicate false, (mean the login user does not has canCreateEntitlementAtAnyBankRole), the userId = login user, + * Here we only can grant the roles which the login user has, we need to find the roles which the login user does not have. + * It will find the not existing ones from the userId's already has entitlements by requestEntitlements.--> throw the no existing error + */ + private def checkRequestRolesForTheUser(userId:String, requestEntitlements: List[CreateEntitlementJSON], isDuplicate:Boolean, callContext: Option[CallContext]) = { + //1st: get all the entitlements for the user: + val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) + val rolesAlreadyHas = entitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet + + val rolesFromRequest = requestEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet + + //2rd: find the duplicated ones: + val duplicatedEntitlements = if(isDuplicate) + rolesAlreadyHas.filter(rolesFromRequest) + else + rolesFromRequest.filterNot(rolesAlreadyHas) + + if(duplicatedEntitlements.size >0){ + val errorMessages = if(isDuplicate) + s"$EntitlementAlreadyExists user_id($userId) ${duplicatedEntitlements.mkString(",")}" + else + s"$EntitlementCannotBeGranted user_id($userId). The login user do not have the following roles yet: ${duplicatedEntitlements.mkString(",")}" + Helper.booleanToFuture(errorMessages, cc=callContext) {false} + }else + Future.successful(Full()) } private def checkRoleBankIdMapping(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { diff --git a/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala index 6fe224600..1dc0f6b1e 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala @@ -98,7 +98,7 @@ class AccountAccessTest extends V400ServerSetup { responseRevoke.body.extract[RevokedJsonV400] { - val postCreateUserJson = PostCreateUserAccountAccessJsonV400(resourceUser2.userId, resourceUser2.provider, List(PostViewJsonV400(view.id, view.is_system))) + val postCreateUserJson = PostCreateUserAccountAccessJsonV400(resourceUser2.userId, "dauth."+resourceUser2.provider, List(PostViewJsonV400(view.id, view.is_system))) When("We send the request") val request = (v4_0_0_Request / "banks" / bankId / "accounts" / account.account_id / "user-account-access").POST <@ (user1) val response = makePostRequest(request, write(postCreateUserJson)) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala index 6b9b6f8c1..f71c62f5c 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala @@ -115,6 +115,62 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { } } + scenario("We try to - createUserWithRoles - not roles, only grant the roles the login user has ", ApiEndpoint3, VersionOfApi) { + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + r.code should equal(400) + r.body.toString contains (EntitlementCannotBeGranted) shouldBe(true) + } + } + + scenario("We try to - createUserWithRoles - short user_id ", ApiEndpoint3, VersionOfApi) { + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(user_id ="xx", roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + r.code should equal(400) + r.body.toString contains (InvalidUserId) shouldBe(true) + } + } + scenario("We try to - createUserWithRoles - wrong user provider ", ApiEndpoint3, VersionOfApi) { + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(provider ="xx", roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + r.code should equal(400) + r.body.toString contains (InvalidUserProvider) shouldBe(true) + } + } + scenario("We try to - createUserWithRoles", ApiEndpoint3, VersionOfApi) { When("We add required entitlement") Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanCreateEntitlementAtAnyBank.toString) From acd490499100077bbf7c0c102671de533360c494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 19 Nov 2021 10:51:17 +0100 Subject: [PATCH 143/185] feature/OpenID Connect; Enable props email_domain_to_space_mappings --- .../src/main/scala/code/api/directlogin.scala | 8 +-- .../main/scala/code/api/openidconnect.scala | 2 + .../code/model/dataAccess/AuthUser.scala | 64 ++++++++++--------- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/obp-api/src/main/scala/code/api/directlogin.scala b/obp-api/src/main/scala/code/api/directlogin.scala index 3dc004a94..dd2196953 100644 --- a/obp-api/src/main/scala/code/api/directlogin.scala +++ b/obp-api/src/main/scala/code/api/directlogin.scala @@ -113,12 +113,8 @@ object DirectLogin extends RestHelper with MdcLoggable { try { val resourceUser = UserX.findByResourceUserId(userId).openOrThrowException(s"$InvalidDirectLoginParameters can not find the resourceUser!") val authUser = AuthUser.findUserByUsernameLocally(resourceUser.name).openOrThrowException(s"$InvalidDirectLoginParameters can not find the auth user!") - if(!emailDomainToSpaceMappings.isEmpty){ - AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser) - } - if(!emailDomainToEntitlementMappings.isEmpty){ - AuthUser.grantEmailDomainEntitlementsToUser(authUser) - } + AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser) + AuthUser.grantEmailDomainEntitlementsToUser(authUser) } catch { case e: Throwable => // error handling, found wrong props value as early as possible. this.logger.error(s"directLogin.grantEntitlementsToUseDynamicEndpointsInSpacesInDirectLogin throw exception, details: $e" ); diff --git a/obp-api/src/main/scala/code/api/openidconnect.scala b/obp-api/src/main/scala/code/api/openidconnect.scala index 09f97ad98..a3b387bec 100644 --- a/obp-api/src/main/scala/code/api/openidconnect.scala +++ b/obp-api/src/main/scala/code/api/openidconnect.scala @@ -124,6 +124,8 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { case Full(authUser) => // Grant roles according to the props email_domain_to_space_mappings AuthUser.grantEmailDomainEntitlementsToUser(authUser) + // Grant roles according to the props email_domain_to_space_mappings + AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser) // Consumer getOrCreateConsumer(idToken, user.userId) match { case Full(consumer) => diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index c0439ec23..d888ab655 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -1154,43 +1154,45 @@ def restoreSomeSessions(): Unit = { } def grantEntitlementsToUseDynamicEndpointsInSpaces(user: AuthUser) = { - val createdByProcess = "grantEntitlementsToUseDynamicEndpointsInSpaces" - val userId = user.user.obj.map(_.userId).getOrElse("") + if(emailDomainToSpaceMappings.nonEmpty) { + val createdByProcess = "grantEntitlementsToUseDynamicEndpointsInSpaces" + val userId = user.user.obj.map(_.userId).getOrElse("") - // user's already auto granted entitlements. - val entitlementsGrantedByThisProcess = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) - .map(_.filter(role => role.createdByProcess == createdByProcess)) - .getOrElse(Nil) + // user's already auto granted entitlements. + val entitlementsGrantedByThisProcess = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) + .map(_.filter(role => role.createdByProcess == createdByProcess)) + .getOrElse(Nil) - def alreadyHasEntitlement(role:ApiRole, bankId: String): Boolean = - entitlementsGrantedByThisProcess.exists(entitlement => entitlement.roleName == role.toString() && entitlement.bankId == bankId) + def alreadyHasEntitlement(role:ApiRole, bankId: String): Boolean = + entitlementsGrantedByThisProcess.exists(entitlement => entitlement.roleName == role.toString() && entitlement.bankId == bankId) - //call mySpaces --> get BankIds --> listOfRolesToUseAllDynamicEndpointsAOneBank (at each bank)--> Grant roles (for each role) - val allCurrentDynamicRoleToBankIdPairs: List[(ApiRole, String)] = for { - BankId(bankId) <- mySpaces(user: AuthUser) - role <- DynamicEndpointHelper.listOfRolesToUseAllDynamicEndpointsAOneBank(Some(bankId)) - } yield { - if (!alreadyHasEntitlement(role, bankId)) { - Entitlement.entitlement.vend.addEntitlement(bankId, userId, role.toString, createdByProcess) + //call mySpaces --> get BankIds --> listOfRolesToUseAllDynamicEndpointsAOneBank (at each bank)--> Grant roles (for each role) + val allCurrentDynamicRoleToBankIdPairs: List[(ApiRole, String)] = for { + BankId(bankId) <- mySpaces(user: AuthUser) + role <- DynamicEndpointHelper.listOfRolesToUseAllDynamicEndpointsAOneBank(Some(bankId)) + } yield { + if (!alreadyHasEntitlement(role, bankId)) { + Entitlement.entitlement.vend.addEntitlement(bankId, userId, role.toString, createdByProcess) + } + + role -> bankId } - role -> bankId - } + // if user's auto granted entitlement invalid, delete it. + // invalid happens when some dynamic endpoints are removed, so the entitlements linked to the deleted dynamic endpoints are invalid. + for { + grantedEntitlement <- entitlementsGrantedByThisProcess + grantedEntitlementRoleName = grantedEntitlement.roleName + grantedEntitlementBankId = grantedEntitlement.bankId + } { + val isInValidEntitlement = !allCurrentDynamicRoleToBankIdPairs.exists { roleToBankIdPair => + val(role, roleBankId) = roleToBankIdPair + role.toString() == grantedEntitlementRoleName && roleBankId == grantedEntitlementBankId + } - // if user's auto granted entitlement invalid, delete it. - // invalid happens when some dynamic endpoints are removed, so the entitlements linked to the deleted dynamic endpoints are invalid. - for { - grantedEntitlement <- entitlementsGrantedByThisProcess - grantedEntitlementRoleName = grantedEntitlement.roleName - grantedEntitlementBankId = grantedEntitlement.bankId - } { - val isInValidEntitlement = !allCurrentDynamicRoleToBankIdPairs.exists { roleToBankIdPair => - val(role, roleBankId) = roleToBankIdPair - role.toString() == grantedEntitlementRoleName && roleBankId == grantedEntitlementBankId - } - - if(isInValidEntitlement) { - Entitlement.entitlement.vend.deleteEntitlement(Full(grantedEntitlement)) + if(isInValidEntitlement) { + Entitlement.entitlement.vend.deleteEntitlement(Full(grantedEntitlement)) + } } } } From b1c07e221b77467e56b0cb553e0bbd991387bec0 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 19 Nov 2021 00:07:46 +0100 Subject: [PATCH 144/185] refactor/typo --- .../scala/code/api/util/ErrorMessages.scala | 3 +- .../scala/code/api/v4_0_0/APIMethods400.scala | 76 ++++++++++++++----- .../code/api/v4_0_0/AccountAccessTest.scala | 2 +- .../code/api/v4_0_0/EntitlementTests.scala | 56 ++++++++++++++ 4 files changed, 115 insertions(+), 22 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index f1c5d1734..9a8b5dcaf 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -193,7 +193,7 @@ object ErrorMessages { val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: " val CannotGetOrCreateUser = "OBP-20102: Cannot get or create user." - val InvalidUserProvider = "OBP-20103: Invalid User Provider." + val InvalidUserProvider = "OBP-20103: Invalid DAuth User Provider." // OAuth 2 val ApplicationNotIdentified = "OBP-20200: The application cannot be identified. " @@ -412,6 +412,7 @@ object ErrorMessages { val EntitlementRequestNotFound = "OBP-30215: EntitlementRequestId not found" val EntitlementAlreadyExists = "OBP-30216: Entitlement already exists for the user." val EntitlementCannotBeDeleted = "OBP-30219: EntitlementId cannot be deleted." + val EntitlementCannotBeGranted = "OBP-30220: Entitlement cannot be granted." val CreateSystemViewError = "OBP-30250: Could not create the system view" val DeleteSystemViewError = "OBP-30251: Could not delete the system view" diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 9bd243779..e5bd41011 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -5,7 +5,7 @@ import code.DynamicEndpoint.DynamicEndpointSwagger import code.accountattribute.AccountAttributeX import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil.{fullBoxOrException, _} -import code.api.util.ApiRole._ +import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, _} import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.ExampleValue._ @@ -2762,10 +2762,10 @@ trait APIMethods400 { EntitlementIsBankRole, EntitlementIsSystemRole, EntitlementAlreadyExists, + InvalidUserProvider, UnknownError ), - List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle), - Some(List(canCreateEntitlementAtOneBank,canCreateEntitlementAtAnyBank))) + List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle)) lazy val createUserWithRoles: OBPEndpoint = { case "user-entitlements" :: Nil JsonPost json -> _ => { @@ -2776,6 +2776,17 @@ trait APIMethods400 { postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { json.extract[PostCreateUserWithRolesJsonV400] } + + //provider must start with dauth., can not create other provider users. + _ <- Helper.booleanToFuture(s"$InvalidUserProvider The user.provider must be start with 'dauth.'", cc=Some(cc)) { + postedData.provider.startsWith("dauth.") + } + + //user_id set the length for the min length of the userId. eg: 36 + _ <- Helper.booleanToFuture(s"$InvalidUserId The user.user_id length must be at least 36. ", cc=Some(cc)) { + postedData.user_id.length>=36 + } + //check the system role bankId is Empty, but bank level role need bankId _ <- checkRoleBankIdMappings(callContext, postedData) @@ -2783,14 +2794,17 @@ trait APIMethods400 { _ <- checkRolesName(callContext, postedData) - _ <- NewStyle.function.hasEntitlement("", loggedInUser.userId, canCreateEntitlementAtAnyBank, cc.callContext) + canCreateEntitlementAtAnyBankRole = Entitlement.entitlement.vend.getEntitlement("", loggedInUser.userId, canCreateEntitlementAtAnyBank.toString()) - (postBodyUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) - - _ <- checkIfUserAlreadyHasEntitlements(postedData, callContext) + (requestUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) + + _ <- if (canCreateEntitlementAtAnyBankRole.isDefined) + checkRequestRolesForTheUser(requestUser.userId, postedData.roles, true, callContext) + else + checkRequestRolesForTheUser(loggedInUser.userId, postedData.roles, false, callContext) addedEntitlements <- addEntitlementsToUser(postedData, callContext) - + } yield { (JSONFactory400.createEntitlementJSONs(addedEntitlements), HttpCode.`201`(callContext)) } @@ -8524,7 +8538,7 @@ trait APIMethods400 { s"""${Glossary.getGlossaryItem("API Collections")} | | - |Delete Api Collection Endpoint By Id + |Delete Api Collection Endpoint By OPERATION_ID | |${authenticationRequiredMessage(true)} | @@ -8598,7 +8612,7 @@ trait APIMethods400 { "Delete My Api Collection Endpoint By Id", s"""${Glossary.getGlossaryItem("API Collections")} |Delete Api Collection Endpoint - |Delete Api Collection Endpoint By IdDelete Api Collection Endpoint + |Delete Api Collection Endpoint By Id | |${authenticationRequiredMessage(true)} | @@ -10909,17 +10923,39 @@ trait APIMethods400 { } private def addEntitlementsToUser(postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { - Future.sequence(postedData.roles.map(addEntitlementToUser(postedData.user_id, _, callContext))) + Future.sequence(postedData.roles.distinct.map(addEntitlementToUser(postedData.user_id, _, callContext))) } - - private def checkIfUserAlreadyHasEntitlement(userId:String, entitlement: CreateEntitlementJSON, callContext: Option[CallContext]) = { - Helper.booleanToFuture(failMsg = s"$EntitlementAlreadyExists Current Entitlement (${entitlement.role_name})", cc=callContext) { - hasEntitlement(entitlement.bank_id, userId, valueOf(entitlement.role_name)) == false - } - } - - private def checkIfUserAlreadyHasEntitlements(postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { - Future.sequence(postedData.roles.map(checkIfUserAlreadyHasEntitlement(postedData.user_id, _, callContext))) + + /** + * this method checks the roles for the userId, + * If isDuplicate = true, (mean the login user has canCreateEntitlementAtAnyBankRole), the userId = requestUserId, + * Here we will grant all the roles to the requestUserId, so first we need to check if the requestUserId already has the roles or not, so + * It will find the duplication ones between requestEntitlements and the userId's already has entitlements.--> throw the duplication error + * If isDuplicate false, (mean the login user does not has canCreateEntitlementAtAnyBankRole), the userId = login user, + * Here we only can grant the roles which the login user has, we need to find the roles which the login user does not have. + * It will find the not existing ones from the userId's already has entitlements by requestEntitlements.--> throw the no existing error + */ + private def checkRequestRolesForTheUser(userId:String, requestEntitlements: List[CreateEntitlementJSON], isDuplicate:Boolean, callContext: Option[CallContext]) = { + //1st: get all the entitlements for the user: + val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) + val rolesAlreadyHas = entitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet + + val rolesFromRequest = requestEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet + + //2rd: find the duplicated ones: + val duplicatedEntitlements = if(isDuplicate) + rolesAlreadyHas.filter(rolesFromRequest) + else + rolesFromRequest.filterNot(rolesAlreadyHas) + + if(duplicatedEntitlements.size >0){ + val errorMessages = if(isDuplicate) + s"$EntitlementAlreadyExists user_id($userId) ${duplicatedEntitlements.mkString(",")}" + else + s"$EntitlementCannotBeGranted user_id($userId). The login user do not have the following roles yet: ${duplicatedEntitlements.mkString(",")}" + Helper.booleanToFuture(errorMessages, cc=callContext) {false} + }else + Future.successful(Full()) } private def checkRoleBankIdMapping(callContext: Option[CallContext], entitlement: CreateEntitlementJSON) = { diff --git a/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala index 6fe224600..1dc0f6b1e 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/AccountAccessTest.scala @@ -98,7 +98,7 @@ class AccountAccessTest extends V400ServerSetup { responseRevoke.body.extract[RevokedJsonV400] { - val postCreateUserJson = PostCreateUserAccountAccessJsonV400(resourceUser2.userId, resourceUser2.provider, List(PostViewJsonV400(view.id, view.is_system))) + val postCreateUserJson = PostCreateUserAccountAccessJsonV400(resourceUser2.userId, "dauth."+resourceUser2.provider, List(PostViewJsonV400(view.id, view.is_system))) When("We send the request") val request = (v4_0_0_Request / "banks" / bankId / "accounts" / account.account_id / "user-account-access").POST <@ (user1) val response = makePostRequest(request, write(postCreateUserJson)) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala index 6b9b6f8c1..f71c62f5c 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala @@ -115,6 +115,62 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { } } + scenario("We try to - createUserWithRoles - not roles, only grant the roles the login user has ", ApiEndpoint3, VersionOfApi) { + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + r.code should equal(400) + r.body.toString contains (EntitlementCannotBeGranted) shouldBe(true) + } + } + + scenario("We try to - createUserWithRoles - short user_id ", ApiEndpoint3, VersionOfApi) { + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(user_id ="xx", roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + r.code should equal(400) + r.body.toString contains (InvalidUserId) shouldBe(true) + } + } + scenario("We try to - createUserWithRoles - wrong user provider ", ApiEndpoint3, VersionOfApi) { + And("We make the request") + val createEntitlements = List(CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanCreateBranch.toString() + ), CreateEntitlementJSON( + bank_id = testBankId1.value, + role_name = CanUpdateBranch.toString() + )) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(provider ="xx", roles= createEntitlements) + val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) + val responseGet = makePostRequestAsync(requestGet, write(postJson)) + Then("We should get a 200") + responseGet map { r => + r.code should equal(400) + r.body.toString contains (InvalidUserProvider) shouldBe(true) + } + } + scenario("We try to - createUserWithRoles", ApiEndpoint3, VersionOfApi) { When("We add required entitlement") Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.CanCreateEntitlementAtAnyBank.toString) From 989b641a85e43c39c990174753a765d53d0fe5e2 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 19 Nov 2021 13:20:45 +0100 Subject: [PATCH 145/185] refactor/separate the checkRequestRolesForTheUser to two methods --- .../scala/code/api/v4_0_0/APIMethods400.scala | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index e5bd41011..6f4c79d56 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -2798,11 +2798,16 @@ trait APIMethods400 { (requestUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) - _ <- if (canCreateEntitlementAtAnyBankRole.isDefined) - checkRequestRolesForTheUser(requestUser.userId, postedData.roles, true, callContext) - else - checkRequestRolesForTheUser(loggedInUser.userId, postedData.roles, false, callContext) - + _ <- if (canCreateEntitlementAtAnyBankRole.isDefined) { + //If the loggedIn User has `CanCreateEntitlementAtAnyBankRole` role, then we can grant all the requestRoles to the requestUser. + //But we must check if the requestUser already has the requestRoles or not. + checkIfUserAlreadyHasTheRequestRoles(requestUser.userId, postedData.roles,callContext) + } else { + //If the loggedIn user does not have the `CanCreateEntitlementAtAnyBankRole` role, we can only grant the roles which the loggedIn user have. + //So we need to check if the rqeuestRoles are beyond the current loggedIn user has. + checkIfUserCanGrantTheRequestRoles(loggedInUser.userId, postedData.roles, callContext) + } + addedEntitlements <- addEntitlementsToUser(postedData, callContext) } yield { @@ -10927,32 +10932,45 @@ trait APIMethods400 { } /** - * this method checks the roles for the userId, - * If isDuplicate = true, (mean the login user has canCreateEntitlementAtAnyBankRole), the userId = requestUserId, - * Here we will grant all the roles to the requestUserId, so first we need to check if the requestUserId already has the roles or not, so - * It will find the duplication ones between requestEntitlements and the userId's already has entitlements.--> throw the duplication error - * If isDuplicate false, (mean the login user does not has canCreateEntitlementAtAnyBankRole), the userId = login user, - * Here we only can grant the roles which the login user has, we need to find the roles which the login user does not have. - * It will find the not existing ones from the userId's already has entitlements by requestEntitlements.--> throw the no existing error + * This method will check all the roles the request user already has and the request roles: + * It will find the roles the requestUser already have, then show the error to the developer. + * (We can not grant the same roles to the request user twice) */ - private def checkRequestRolesForTheUser(userId:String, requestEntitlements: List[CreateEntitlementJSON], isDuplicate:Boolean, callContext: Option[CallContext]) = { + private def checkIfUserAlreadyHasTheRequestRoles(requestUserId:String, requestEntitlements: List[CreateEntitlementJSON], callContext: Option[CallContext]) = { //1st: get all the entitlements for the user: - val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) + val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(requestUserId) val rolesAlreadyHas = entitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet val rolesFromRequest = requestEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet //2rd: find the duplicated ones: - val duplicatedEntitlements = if(isDuplicate) - rolesAlreadyHas.filter(rolesFromRequest) - else - rolesFromRequest.filterNot(rolesAlreadyHas) + val duplicatedEntitlements = rolesAlreadyHas.filter(rolesFromRequest) + + //3rd: We can not grant the roles again, so we show the error to the developer. + if(duplicatedEntitlements.size >0){ + val errorMessages = s"$EntitlementAlreadyExists user_id($requestUserId) ${duplicatedEntitlements.mkString(",")}" + Helper.booleanToFuture(errorMessages, cc=callContext) {false} + }else + Future.successful(Full()) + } + + /** + * This method will check all the roles the loggedIn user already has and the request roles: + * It will find the not existing roles from the loggedIn user --> we will show the error to the developer + * (We can only grant the roles which the loggedIn User has to the requestUser) + */ + private def checkIfUserCanGrantTheRequestRoles(loggedInUserId:String, requestEntitlements: List[CreateEntitlementJSON], callContext: Option[CallContext]) = { + //1st: get all the entitlements for the user: + val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(loggedInUserId) + val rolesAlreadyHas = entitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet + + val rolesFromRequest = requestEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet + + //2rd: find the roles which the loggedIn user does not have, + val duplicatedEntitlements = rolesFromRequest.filterNot(rolesAlreadyHas) if(duplicatedEntitlements.size >0){ - val errorMessages = if(isDuplicate) - s"$EntitlementAlreadyExists user_id($userId) ${duplicatedEntitlements.mkString(",")}" - else - s"$EntitlementCannotBeGranted user_id($userId). The login user do not have the following roles yet: ${duplicatedEntitlements.mkString(",")}" + val errorMessages = s"$EntitlementCannotBeGranted user_id($loggedInUserId). The login user do not have the following roles yet: ${duplicatedEntitlements.mkString(",")}" Helper.booleanToFuture(errorMessages, cc=callContext) {false} }else Future.successful(Full()) From f0eced7ec6e419ccd55b414d2e9dbe622d4d4770 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 19 Nov 2021 15:04:40 +0100 Subject: [PATCH 146/185] refactor/rename the methods and variables --- .../scala/code/api/util/ExampleValue.scala | 2 +- .../scala/code/api/v4_0_0/APIMethods400.scala | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index c8b5aa220..ae76c49ab 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -2061,7 +2061,7 @@ object ExampleValue { lazy val indexExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("index", indexExample) - lazy val descriptionExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val descriptionExample = ConnectorField("This is used for customer x!","The human readable description here.") glossaryItems += makeGlossaryItem("description", descriptionExample) lazy val dynamicResourceDocdescriptionExample = ConnectorField("Create one User", "the description for this endpoint") diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 6f4c79d56..3746044c2 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -2796,16 +2796,16 @@ trait APIMethods400 { canCreateEntitlementAtAnyBankRole = Entitlement.entitlement.vend.getEntitlement("", loggedInUser.userId, canCreateEntitlementAtAnyBank.toString()) - (requestUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) + (targetUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) _ <- if (canCreateEntitlementAtAnyBankRole.isDefined) { //If the loggedIn User has `CanCreateEntitlementAtAnyBankRole` role, then we can grant all the requestRoles to the requestUser. //But we must check if the requestUser already has the requestRoles or not. - checkIfUserAlreadyHasTheRequestRoles(requestUser.userId, postedData.roles,callContext) + assertTargetUserLacksRoles(targetUser.userId, postedData.roles,callContext) } else { //If the loggedIn user does not have the `CanCreateEntitlementAtAnyBankRole` role, we can only grant the roles which the loggedIn user have. - //So we need to check if the rqeuestRoles are beyond the current loggedIn user has. - checkIfUserCanGrantTheRequestRoles(loggedInUser.userId, postedData.roles, callContext) + //So we need to check if the requestRoles are beyond the current loggedIn user has. + assertUserCanGrantRoles(loggedInUser.userId, postedData.roles, callContext) } addedEntitlements <- addEntitlementsToUser(postedData, callContext) @@ -10936,19 +10936,19 @@ trait APIMethods400 { * It will find the roles the requestUser already have, then show the error to the developer. * (We can not grant the same roles to the request user twice) */ - private def checkIfUserAlreadyHasTheRequestRoles(requestUserId:String, requestEntitlements: List[CreateEntitlementJSON], callContext: Option[CallContext]) = { + private def assertTargetUserLacksRoles(userId:String, requestedEntitlements: List[CreateEntitlementJSON], callContext: Option[CallContext]) = { //1st: get all the entitlements for the user: - val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(requestUserId) - val rolesAlreadyHas = entitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet + val userEntitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) + val userRoles = userEntitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet - val rolesFromRequest = requestEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet + val targetRoles = requestedEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet //2rd: find the duplicated ones: - val duplicatedEntitlements = rolesAlreadyHas.filter(rolesFromRequest) + val duplicatedRoles = userRoles.filter(targetRoles) //3rd: We can not grant the roles again, so we show the error to the developer. - if(duplicatedEntitlements.size >0){ - val errorMessages = s"$EntitlementAlreadyExists user_id($requestUserId) ${duplicatedEntitlements.mkString(",")}" + if(duplicatedRoles.size >0){ + val errorMessages = s"$EntitlementAlreadyExists user_id($userId) ${duplicatedRoles.mkString(",")}" Helper.booleanToFuture(errorMessages, cc=callContext) {false} }else Future.successful(Full()) @@ -10959,18 +10959,18 @@ trait APIMethods400 { * It will find the not existing roles from the loggedIn user --> we will show the error to the developer * (We can only grant the roles which the loggedIn User has to the requestUser) */ - private def checkIfUserCanGrantTheRequestRoles(loggedInUserId:String, requestEntitlements: List[CreateEntitlementJSON], callContext: Option[CallContext]) = { + private def assertUserCanGrantRoles(userId:String, requestedEntitlements: List[CreateEntitlementJSON], callContext: Option[CallContext]) = { //1st: get all the entitlements for the user: - val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(loggedInUserId) - val rolesAlreadyHas = entitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet + val userEntitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(userId) + val userRoles = userEntitlements.map(_.map(entitlement => (entitlement.roleName, entitlement.bankId))).getOrElse(List.empty[(String,String)]).toSet - val rolesFromRequest = requestEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet + val targetRoles = requestedEntitlements.map(entitlement => (entitlement.role_name, entitlement.bank_id)).toSet //2rd: find the roles which the loggedIn user does not have, - val duplicatedEntitlements = rolesFromRequest.filterNot(rolesAlreadyHas) + val roleLacking = targetRoles.filterNot(userRoles) - if(duplicatedEntitlements.size >0){ - val errorMessages = s"$EntitlementCannotBeGranted user_id($loggedInUserId). The login user do not have the following roles yet: ${duplicatedEntitlements.mkString(",")}" + if(roleLacking.size >0){ + val errorMessages = s"$EntitlementCannotBeGranted user_id($userId). The login user does not have the following roles: ${roleLacking.mkString(",")}" Helper.booleanToFuture(errorMessages, cc=callContext) {false} }else Future.successful(Full()) From 9990ca7cb47e113e302a264262ec48f2bea5ccdf Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Fri, 19 Nov 2021 21:00:50 +0100 Subject: [PATCH 147/185] /docfix Added DAuth tag, enhanced DAuth related endpoints --- .../src/main/scala/code/api/util/ApiTag.scala | 2 + .../main/scala/code/api/util/Glossary.scala | 2 +- .../scala/code/api/v4_0_0/APIMethods400.scala | 45 +++++++++++++++---- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index d03c323a9..14d135ac3 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -77,6 +77,8 @@ object ApiTag { val apiTagDynamicResourceDoc = ResourceDocTag("Dynamic-Resource-Doc") val apiTagDynamicMessageDoc = ResourceDocTag("Dynamic-Message-Doc") + val apiTagDAuth = ResourceDocTag("DAuth") + val apiTagDynamic = ResourceDocTag("Dynamic") val apiTagDynamicEntity = ResourceDocTag("Dynamic-Entity") val apiTagManageDynamicEntity = ResourceDocTag("Dynamic-Entity-Manage") 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 4be54dfca..978af487b 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -2040,7 +2040,7 @@ object Glossary extends MdcLoggable { | |If you are calling OBP-API via an API3 Airnode, the Airnode will take care of constructing the required header. | -|When OBP detects an DAuth header / token it first checks if the Consumer is allowed to make such a call. OBP will validate the Consumer ip address and signature etc. +|When OBP detects a DAuth header / token it first checks if the Consumer is allowed to make such a call. OBP will validate the Consumer ip address and signature etc. | |Note: The DAuth flow does *not* require an explicit POST like Direct Login to create the token. | diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 3746044c2..89508b07d 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -82,6 +82,8 @@ import java.net.URLEncoder import java.text.SimpleDateFormat import java.util.{Calendar, Date} +import code.api.util.Glossary.getGlossaryItem + import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future @@ -2741,17 +2743,34 @@ trait APIMethods400 { nameOf(createUserWithRoles), "POST", "/user-entitlements", - "Create User with Roles", - """This will create the User with user_id and provider if it does not exist + "Create (DAuth) User with Roles", + s""" + |This endpoint is used as part of the DAuth solution to grant Entitlements for Roles to a smart contract on the blockchain. | - |Create Entitlement. Grant Role to User. + |Put the smart contract address in user_id + | + |For provider use "dauth" + | + |This endpoint will create the User with user_id and provider if the User does not already exist. + | + |Then it will create Entitlements i.e. grant Roles to the User. | |Entitlements are used to grant System or Bank level roles to Users. (For Account level privileges, see Views) | + |i.e. Entitlements are used to create / consume system or bank level resources where as views / account access are used to consume / create customer level resources. + | |For a System level Role (.e.g CanGetAnyUser), set bank_id to an empty string i.e. "bank_id":"" | |For a Bank level Role (e.g. CanCreateAccount), set bank_id to a valid value e.g. "bank_id":"my-bank-id" | + |Note: The Roles actually granted will depend on the Roles that the calling user has. + | + |If you try to grant Entitlements to a user that already exist (duplicate entitilements) you will get an error. + | + |For information about DAuth see below: + | + |${getGlossaryItem("DAuth")} + | |""", postCreateUserWithRolesJsonV400, entitlementsJsonV400, @@ -2765,7 +2784,7 @@ trait APIMethods400 { InvalidUserProvider, UnknownError ), - List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle)) + List(apiTagRole, apiTagEntitlement, apiTagUser, apiTagNewStyle, apiTagDAuth)) lazy val createUserWithRoles: OBPEndpoint = { case "user-entitlements" :: Nil JsonPost json -> _ => { @@ -4249,10 +4268,20 @@ trait APIMethods400 { nameOf(createUserWithAccountAccess), "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/user-account-access", - "Create User with Account Access", - s"""This will create the User with user_id and provider if it does not exist . + "Create (DAuth) User with Account Access", + s"""This endpoint is used as part of the DAuth solution to grant access to account and transaction data to a smart contract on the blockchain. | - |${authenticationRequiredMessage(true)} and the loggedin user needs to be account holder. + |Put the smart contract address in user_id + | + |For provider use "dauth" + | + |This endpoint will create the (DAuth) User with user_id and provider if the User does not already exist. + | + |${authenticationRequiredMessage(true)} and the logged in user needs to be account holder. + | + |For information about DAuth see below: + | + |${getGlossaryItem("DAuth")} | |""", postCreateUserWithRolesJsonV400, @@ -4266,7 +4295,7 @@ trait APIMethods400 { CannotGrantAccountAccess, UnknownError ), - List(apiTagAccountAccess, apiTagView, apiTagAccount, apiTagUser, apiTagOwnerRequired, apiTagNewStyle)) + List(apiTagAccountAccess, apiTagView, apiTagAccount, apiTagUser, apiTagOwnerRequired, apiTagDAuth, apiTagNewStyle)) lazy val createUserWithAccountAccess : OBPEndpoint = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: "user-account-access" :: Nil JsonPost json -> _ => { From c2b4ea66013b57b489ccc4640f48a02b2a704972 Mon Sep 17 00:00:00 2001 From: hongwei Date: Sat, 20 Nov 2021 11:49:57 +0100 Subject: [PATCH 148/185] feature/added the `dauth.` to all the Dauth provider --- obp-api/src/main/scala/code/api/dauth.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala index b1c8e7fcb..f4f0d9b88 100755 --- a/obp-api/src/main/scala/code/api/dauth.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -136,12 +136,13 @@ object DAuth extends RestHelper with MdcLoggable { def getOrCreateResourceUser(jwtPayload: String, callContext: Option[CallContext]) : Box[(User, Option[CallContext])] = { val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") val provider = getFieldFromPayloadJson(jwtPayload, "network_name") + val providerHardCodePrefixDauth = "dauth."+provider logger.debug("login_user_name: " + username) for { tuple <- - Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = username).or { // Find a user + Users.users.vend.getUserByProviderId(provider = providerHardCodePrefixDauth, idGivenByProvider = username).or { // Find a user Users.users.vend.createResourceUser( // Otherwise create a new one - provider = provider, + provider = providerHardCodePrefixDauth, providerId = Some(username), None, name = Some(username), @@ -168,10 +169,11 @@ object DAuth extends RestHelper with MdcLoggable { def getOrCreateResourceUserFuture(jwtPayload: String, callContext: Option[CallContext]) : Future[Box[(User, Option[CallContext])]] = { val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") val provider = getFieldFromPayloadJson(jwtPayload, "network_name") + val providerHardCodePrefixDauth = "dauth."+provider logger.debug("login_user_name: " + username) for { tuple <- - Users.users.vend.getOrCreateUserByProviderIdFuture(provider = provider, idGivenByProvider = username, consentId = None, name = Some(username), email = None) map { + Users.users.vend.getOrCreateUserByProviderIdFuture(provider = providerHardCodePrefixDauth, idGivenByProvider = username, consentId = None, name = Some(username), email = None) map { case (Full(u), _) => Full(u, callContext) // Return user case (Empty, _) => @@ -223,8 +225,9 @@ object DAuth extends RestHelper with MdcLoggable { case Full(payload) => val username = getFieldFromPayloadJson(payload, "smart_contract_address") val provider = getFieldFromPayloadJson(payload, "network_name") + val providerHardCodePrefixDauth = "dauth."+provider logger.debug("username: " + username) - Users.users.vend.getUserByProviderId(provider = provider, idGivenByProvider = username) + Users.users.vend.getUserByProviderId(provider = providerHardCodePrefixDauth, idGivenByProvider = username) case _ => None } From ab979f837d5b755c61a8eff2fe8df394a711d52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 23 Nov 2021 12:21:07 +0100 Subject: [PATCH 149/185] fature/Tweak endpoint createMyApiCollection v4.0.0 --- .../code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala | 2 +- obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala | 2 +- obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 2712cd229..412182782 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4265,7 +4265,7 @@ object SwaggerDefinitionsJSON { charge = transactionRequestChargeJsonV200 ) - val postApiCollectionJson400 = PostApiCollectionJson400(apiCollectionNameExample.value, true, descriptionExample.value) + val postApiCollectionJson400 = PostApiCollectionJson400(apiCollectionNameExample.value, true, Some(descriptionExample.value)) val apiCollectionJson400 = ApiCollectionJson400(apiCollectionIdExample.value, userIdExample.value, apiCollectionNameExample.value, true, descriptionExample.value) val apiCollectionsJson400 = ApiCollectionsJson400(List(apiCollectionJson400)) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 89508b07d..190a6e612 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -8087,7 +8087,7 @@ trait APIMethods400 { cc.userId, postJson.api_collection_name, postJson.is_sharable, - postJson.description, + postJson.description.getOrElse(""), Some(cc) ) } yield { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index 3e59b2237..ae3685578 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -691,7 +691,7 @@ case class ApiCollectionsJson400 ( case class PostApiCollectionJson400( api_collection_name: String, is_sharable: Boolean, - description: String + description: Option[String] ) case class ApiCollectionEndpointJson400 ( From 0c00bee4a553d4b2a6ee90a7ef58029e775d4866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 23 Nov 2021 14:18:14 +0100 Subject: [PATCH 150/185] refactor/Rename ApiAuth to AfterApiAuth --- obp-api/src/main/scala/code/api/util/APIUtil.scala | 4 ++-- .../scala/code/api/util/{ApiAuth.scala => AfterApiAuth.scala} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename obp-api/src/main/scala/code/api/util/{ApiAuth.scala => AfterApiAuth.scala} (99%) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index d7af26f6a..e47be8c25 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2766,9 +2766,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ // COMMON POST AUTHENTICATION CODE GOES BELOW // Check is it a user deleted or locked - val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = ApiAuth.checkUserIsDeletedOrLocked(res) + val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(res) // Check Rate Limiting - val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = ApiAuth.checkRateLimiting(userIsLockedOrDeleted) + val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkRateLimiting(userIsLockedOrDeleted) // Update Call Context resultWithRateLimiting map { diff --git a/obp-api/src/main/scala/code/api/util/ApiAuth.scala b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala similarity index 99% rename from obp-api/src/main/scala/code/api/util/ApiAuth.scala rename to obp-api/src/main/scala/code/api/util/AfterApiAuth.scala index 5acc2bca7..50cba999c 100644 --- a/obp-api/src/main/scala/code/api/util/ApiAuth.scala +++ b/obp-api/src/main/scala/code/api/util/AfterApiAuth.scala @@ -13,7 +13,7 @@ import com.openbankproject.commons.ExecutionContext.Implicits.global import scala.concurrent.Future -object ApiAuth { +object AfterApiAuth { def checkUserIsDeletedOrLocked(res: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = { for { (user: Box[User], cc) <- res From 84ccd9eb3dfead9de7b70c46e53cb5f92ae4fcc2 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 23 Nov 2021 20:49:16 +0100 Subject: [PATCH 151/185] feature/remove the space for the buildOperationId method --- obp-api/src/main/scala/code/api/util/APIUtil.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index e47be8c25..a51b727fc 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -1666,7 +1666,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } def buildOperationId(apiVersion: ScannedApiVersion, partialFunctionName: String) = - s"${apiVersion.fullyQualifiedVersion}-$partialFunctionName" + s"${apiVersion.fullyQualifiedVersion}-$partialFunctionName".trim //This is correct: OBPv3.0.0-getCoreAccountById //This is OBPv4_0_0-dynamicEntity_deleteFooBar33 From 9abda6462d14f555c68ff872c7c4e229ab1e56e3 Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 24 Nov 2021 12:08:19 +0100 Subject: [PATCH 152/185] docfix/tweaked the document for description field --- obp-api/src/main/scala/code/api/util/ExampleValue.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index ae76c49ab..6fb6cf34f 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -2,9 +2,10 @@ package code.api.util import code.api.util.APIUtil.parseDate -import code.api.util.ErrorMessages.{InvalidJsonFormat, UserHasMissingRoles, UserNotLoggedIn, UnknownError} +import code.api.util.ErrorMessages.{InvalidJsonFormat, UnknownError, UserHasMissingRoles, UserNotLoggedIn} import net.liftweb.json.JsonDSL._ import code.api.util.Glossary.{glossaryItems, makeGlossaryItem} +import code.apicollection.ApiCollection import code.dynamicEntity.{DynamicEntityDefinition, DynamicEntityFooBar, DynamicEntityFullBarFields, DynamicEntityIntTypeExample, DynamicEntityStringTypeExample} import com.openbankproject.commons.model.enums.{CustomerAttributeType, DynamicEntityFieldType} import com.openbankproject.commons.util.ReflectUtils @@ -2061,7 +2062,7 @@ object ExampleValue { lazy val indexExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("index", indexExample) - lazy val descriptionExample = ConnectorField("This is used for customer x!","The human readable description here.") + lazy val descriptionExample = ConnectorField(s"This an optional field. Maximum length is ${ApiCollection.Description.maxLen}. It can be any characters here.","The human readable description here.") glossaryItems += makeGlossaryItem("description", descriptionExample) lazy val dynamicResourceDocdescriptionExample = ConnectorField("Create one User", "the description for this endpoint") From 10acb96ead361d024ee0fdd62dbe45ea98a86eab Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 24 Nov 2021 12:09:13 +0100 Subject: [PATCH 153/185] refactor/typo --- .../code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala | 2 +- obp-api/src/main/scala/code/api/util/ExampleValue.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 412182782..0ff28d9b8 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4285,7 +4285,7 @@ object SwaggerDefinitionsJSON { requestVerb = requestVerbExample.value, requestUrl = requestUrlExample.value, summary = dynamicResourceDocSummaryExample.value, - description = dynamicResourceDocdescriptionExample.value, + description = dynamicResourceDocDescriptionExample.value, exampleRequestBody = Option(json.parse(exampleRequestBodyExample.value)), successResponseBody = Option(json.parse(successResponseBodyExample.value)), errorResponseBodies = errorResponseBodiesExample.value, diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index 6fb6cf34f..3f1ca6743 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -2065,8 +2065,8 @@ object ExampleValue { lazy val descriptionExample = ConnectorField(s"This an optional field. Maximum length is ${ApiCollection.Description.maxLen}. It can be any characters here.","The human readable description here.") glossaryItems += makeGlossaryItem("description", descriptionExample) - lazy val dynamicResourceDocdescriptionExample = ConnectorField("Create one User", "the description for this endpoint") - glossaryItems += makeGlossaryItem("DynamicResourceDoc.description", dynamicResourceDocdescriptionExample) + lazy val dynamicResourceDocDescriptionExample = ConnectorField("Create one User", "the description for this endpoint") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.description", dynamicResourceDocDescriptionExample) lazy val canDeleteCommentExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("can_delete_comment", canDeleteCommentExample) From 7ba30cff511cf5fc7ccb69bb0fceeb9bd4b38e53 Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 24 Nov 2021 14:16:21 +0100 Subject: [PATCH 154/185] feature/added the CanCreateEntitlementAtOneBank role when createBank --- .../main/scala/code/api/v2_2_0/APIMethods220.scala | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala b/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala index ab31a4e38..09ba46f1e 100644 --- a/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala +++ b/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala @@ -1,7 +1,6 @@ package code.api.v2_2_0 import java.util.Date - import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil._ import code.api.util.ApiRole.{canCreateBranch, _} @@ -14,6 +13,7 @@ import code.api.v2_1_0._ import code.api.v2_2_0.JSONFactory220.transformV220ToBranch import code.bankconnectors._ import code.consumer.Consumers +import code.entitlement.Entitlement import code.fx.{MappedFXRate, fx} import code.metadata.counterparties.{Counterparties, MappedCounterparty} import code.metrics.ConnectorMetricsProvider @@ -455,6 +455,16 @@ trait APIMethods220 { bank.bank_routing.scheme, bank.bank_routing.address ) + entitlements <- Entitlement.entitlement.vend.getEntitlementsByUserId(u.userId) + + entitlementsByBank = entitlements.filter(_.bankId==bank.id) + _ <- entitlementsByBank.filter(_.roleName == CanCreateEntitlementAtOneBank.toString()).size > 0 match { + case true => + // Already has entitlement + Full() + case false => + Full(Entitlement.entitlement.vend.addEntitlement(bank.id, u.userId, CanCreateEntitlementAtOneBank.toString())) + } } yield { val json = JSONFactory220.createBankJSON(success) createdJsonResponse(Extraction.decompose(json)) From 476f6157a958a129f20bd3961fe7c862f66abc73 Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 24 Nov 2021 15:26:29 +0100 Subject: [PATCH 155/185] feature/added the missing roles for the CreateBank endpoint --- obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala | 7 +++++++ obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala b/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala index 09ba46f1e..8e4969bbe 100644 --- a/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala +++ b/obp-api/src/main/scala/code/api/v2_2_0/APIMethods220.scala @@ -465,6 +465,13 @@ trait APIMethods220 { case false => Full(Entitlement.entitlement.vend.addEntitlement(bank.id, u.userId, CanCreateEntitlementAtOneBank.toString())) } + _ <- entitlementsByBank.filter(_.roleName == CanReadDynamicResourceDocsAtOneBank.toString()).size > 0 match { + case true => + // Already has entitlement + Full() + case false => + Full(Entitlement.entitlement.vend.addEntitlement(bank.id, u.userId, CanReadDynamicResourceDocsAtOneBank.toString())) + } } yield { val json = JSONFactory220.createBankJSON(success) createdJsonResponse(Extraction.decompose(json)) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 190a6e612..a074d7c89 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -3947,6 +3947,13 @@ trait APIMethods400 { case false => Future(Entitlement.entitlement.vend.addEntitlement(bank.id, cc.userId, CanCreateEntitlementAtOneBank.toString())) } + _ <- entitlementsByBank.filter(_.roleName == CanReadDynamicResourceDocsAtOneBank.toString()).size > 0 match { + case true => + // Already has entitlement + Future() + case false => + Future(Entitlement.entitlement.vend.addEntitlement(bank.id, cc.userId, CanReadDynamicResourceDocsAtOneBank.toString())) + } } yield { (JSONFactory400.createBankJSON400(success), HttpCode.`201`(callContext)) } From 984709e8909ffeee4248b44dd1922d78fe04b10c Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 24 Nov 2021 16:09:32 +0100 Subject: [PATCH 156/185] docfix/tweaked the docs for transaction meta --- .../scala/code/api/v1_2_1/APIMethods121.scala | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala index 3e45426b8..2e5adbe69 100644 --- a/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -2401,7 +2401,7 @@ trait APIMethods121 { "getTransactionNarrative", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative", - "Get narrative", + "Get a Transaction Narrative", """Returns the account owner description of the transaction [moderated](#1_2_1-getViewsForBankAccount) by the view. | |Authentication via OAuth is required if the view is not public.""", @@ -2437,7 +2437,7 @@ trait APIMethods121 { "addTransactionNarrative", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative", - "Add narrative", + "Add a Transaction Narrative", s"""Creates a description of the transaction TRANSACTION_ID. | |Note: Unlike other items of metadata, there is only one "narrative" per transaction accross all views. @@ -2481,7 +2481,7 @@ trait APIMethods121 { "updateTransactionNarrative", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative", - "Update narrative", + "Update a Transaction Narrative", """Updates the description of the transaction TRANSACTION_ID. | |Authentication via OAuth is required if the view is not public.""", @@ -2519,7 +2519,7 @@ trait APIMethods121 { "deleteTransactionNarrative", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/narrative", - "Delete narrative", + "Delete a Transaction Narrative", """Deletes the description of the transaction TRANSACTION_ID. | |Authentication via OAuth is required if the view is not public.""", @@ -2556,7 +2556,7 @@ trait APIMethods121 { "getCommentsForViewOnTransaction", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/comments", - "Get comments", + "Get Transaction Comments", """Returns the transaction TRANSACTION_ID comments made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). | |Authentication via OAuth is required if the view is not public.""", @@ -2593,7 +2593,7 @@ trait APIMethods121 { "addCommentForViewOnTransaction", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/comments", - "Add comment", + "Add a Transaction Comment", """Posts a comment about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. | |${authenticationRequiredMessage(false)} @@ -2639,7 +2639,7 @@ trait APIMethods121 { "deleteCommentForViewOnTransaction", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/comments/COMMENT_ID", - "Delete comment", + "Delete a Transaction Comment", """Delete the comment COMMENT_ID about the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). | |Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the comment.""", @@ -2677,7 +2677,7 @@ trait APIMethods121 { "getTagsForViewOnTransaction", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags", - "Get tags", + "Get Transaction Tags", """Returns the transaction TRANSACTION_ID tags made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). Authentication via OAuth is required if the view is not public.""", emptyObjectJson, @@ -2713,7 +2713,7 @@ trait APIMethods121 { "addTagForViewOnTransaction", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags", - "Add a tag", + "Add a Transaction Tag", s"""Posts a tag about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. | |${authenticationRequiredMessage(true)} @@ -2759,7 +2759,7 @@ trait APIMethods121 { "deleteTagForViewOnTransaction", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/tags/TAG_ID", - "Delete a tag", + "Delete a Transaction Tag", """Deletes the tag TAG_ID about the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). |Authentication via OAuth is required. The user must either have owner privileges for this account, |or must be the user that posted the tag. @@ -2796,7 +2796,7 @@ trait APIMethods121 { "getImagesForViewOnTransaction", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images", - "Get images", + "Get Transaction Images", """Returns the transaction TRANSACTION_ID images made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). Authentication via OAuth is required if the view is not public.""", emptyObjectJson, @@ -2832,7 +2832,7 @@ trait APIMethods121 { "addImageForViewOnTransaction", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images", - "Add an image", + "Add a Transaction Image", s"""Posts an image about a transaction TRANSACTION_ID on a [view](#1_2_1-getViewsForBankAccount) VIEW_ID. | |${authenticationRequiredMessage(true) } @@ -2878,7 +2878,7 @@ trait APIMethods121 { "deleteImageForViewOnTransaction", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/images/IMAGE_ID", - "Delete an image", + "Delete a Transaction Image", """Deletes the image IMAGE_ID about the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). | |Authentication via OAuth is required. The user must either have owner privileges for this account, or must be the user that posted the image.""", @@ -2919,7 +2919,7 @@ trait APIMethods121 { "getWhereTagForViewOnTransaction", "GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", - "Get where tag", + "Get a Transaction where Tag", """Returns the "where" Geo tag added to the transaction TRANSACTION_ID made on a [view](#1_2_1-getViewsForBankAccount) (VIEW_ID). |It represents the location where the transaction has been initiated. | @@ -2956,7 +2956,7 @@ trait APIMethods121 { "addWhereTagForViewOnTransaction", "POST", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", - "Add where tag", + "Add a Transaction where Tag", s"""Creates a "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount). | |${authenticationRequiredMessage(true)} @@ -3002,7 +3002,7 @@ trait APIMethods121 { "updateWhereTagForViewOnTransaction", "PUT", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", - "Update where tag", + "Update a Transaction where Tag", s"""Updates the "where" Geo tag on a transaction TRANSACTION_ID in a [view](#1_2_1-getViewsForBankAccount). | |${authenticationRequiredMessage(true)} @@ -3048,7 +3048,7 @@ trait APIMethods121 { "deleteWhereTagForViewOnTransaction", "DELETE", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions/TRANSACTION_ID/metadata/where", - "Delete where tag", + "Delete a Transaction Tag", s"""Deletes the where tag of the transaction TRANSACTION_ID made on [view](#1_2_1-getViewsForBankAccount). | |${authenticationRequiredMessage(true)} From f637f48095dd0766ce4b7a24506c7bc4c6332215 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 11:42:03 +0100 Subject: [PATCH 157/185] bugfix/Unexpected missing generic font family --- .../webapp/media/css/api-documentation-content.css | 6 +++--- obp-api/src/main/webapp/media/css/authorise.css | 12 ++++++------ .../src/main/webapp/media/css/cookies-consent.css | 6 +++--- obp-api/src/main/webapp/media/css/data-area.css | 8 ++++---- obp-api/src/main/webapp/media/css/fonts.css | 2 +- obp-api/src/main/webapp/media/css/footer.css | 2 +- obp-api/src/main/webapp/media/css/get-started.css | 6 +++--- obp-api/src/main/webapp/media/css/main-apis.css | 4 ++-- obp-api/src/main/webapp/media/css/main-faq.css | 6 +++--- obp-api/src/main/webapp/media/css/main-showcases.css | 4 ++-- obp-api/src/main/webapp/media/css/main-start.css | 2 +- obp-api/src/main/webapp/media/css/main-support.css | 2 +- obp-api/src/main/webapp/media/css/nav.css | 8 ++++---- obp-api/src/main/webapp/media/css/obp-toastr.css | 2 +- .../src/main/webapp/media/css/recover-password.css | 2 +- .../src/main/webapp/media/css/register-consumer.css | 8 ++++---- obp-api/src/main/webapp/media/css/signup.css | 12 ++++++------ obp-api/src/main/webapp/media/css/website.css | 12 ++++++------ 18 files changed, 52 insertions(+), 52 deletions(-) diff --git a/obp-api/src/main/webapp/media/css/api-documentation-content.css b/obp-api/src/main/webapp/media/css/api-documentation-content.css index 0f87dab5a..1c0dffa6e 100644 --- a/obp-api/src/main/webapp/media/css/api-documentation-content.css +++ b/obp-api/src/main/webapp/media/css/api-documentation-content.css @@ -48,7 +48,7 @@ font-size: 16px; line-height: 24px; font-weight:normal; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; /*paragraph-height: 12px*/ } @@ -58,12 +58,12 @@ line-height: 24px; font-weight:normal; margin: 8px 0; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; /*paragraph-height: 12px*/ } .container #api_documentation_content a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/authorise.css b/obp-api/src/main/webapp/media/css/authorise.css index 60a886e0a..714ac67e4 100644 --- a/obp-api/src/main/webapp/media/css/authorise.css +++ b/obp-api/src/main/webapp/media/css/authorise.css @@ -18,7 +18,7 @@ #login-form-password{ margin-bottom: 8px; margin-top: 32px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -31,7 +31,7 @@ float: right; height: 44px; width: 92px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; text-align: center; line-height: 24px; @@ -73,7 +73,7 @@ } #authorise #authorise-recover-password a, #authorise #oauth-authorise-recover-password a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; letter-spacing: 0; @@ -84,7 +84,7 @@ #authorise .login-or { margin-top: 30px; float: left; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 18px; color: #929292; letter-spacing: 0; @@ -110,7 +110,7 @@ } #authorise #thanks-detail{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -142,7 +142,7 @@ #authorise #login-form-username-error, #authorise #login-form-password-error{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; line-height: 20px; diff --git a/obp-api/src/main/webapp/media/css/cookies-consent.css b/obp-api/src/main/webapp/media/css/cookies-consent.css index 6ce328364..5a865c0af 100644 --- a/obp-api/src/main/webapp/media/css/cookies-consent.css +++ b/obp-api/src/main/webapp/media/css/cookies-consent.css @@ -1,6 +1,6 @@ #cookies-consent { background: #252525; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; display:none; text-align: center; @@ -17,7 +17,7 @@ position: absolute; left: 100px; top: 32px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; line-height: 24px; @@ -30,7 +30,7 @@ position: absolute; left: 100px; top: 80px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; text-align: center; diff --git a/obp-api/src/main/webapp/media/css/data-area.css b/obp-api/src/main/webapp/media/css/data-area.css index a3cd199ca..82a0dc0cc 100644 --- a/obp-api/src/main/webapp/media/css/data-area.css +++ b/obp-api/src/main/webapp/media/css/data-area.css @@ -40,7 +40,7 @@ #data-area #data-area-explanation p:last-child{ margin-top: 8px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; letter-spacing: 0; @@ -51,7 +51,7 @@ #data-area-input form label{ margin-bottom: 8px; margin-top: 17px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -114,7 +114,7 @@ #data-area-success span, #data-area-success a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -138,7 +138,7 @@ } #data-area-input #data-area-errors{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; line-height: 20px; diff --git a/obp-api/src/main/webapp/media/css/fonts.css b/obp-api/src/main/webapp/media/css/fonts.css index d95c71b25..3120bb343 100644 --- a/obp-api/src/main/webapp/media/css/fonts.css +++ b/obp-api/src/main/webapp/media/css/fonts.css @@ -11,7 +11,7 @@ } @font-face { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; src: url(../font/Roboto-Regular.ttf); } diff --git a/obp-api/src/main/webapp/media/css/footer.css b/obp-api/src/main/webapp/media/css/footer.css index f6069c310..738c9cb51 100644 --- a/obp-api/src/main/webapp/media/css/footer.css +++ b/obp-api/src/main/webapp/media/css/footer.css @@ -9,7 +9,7 @@ footer a,span, footer a:hover, footer a:focus, span:hover { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #FFFFFF; letter-spacing: 0; diff --git a/obp-api/src/main/webapp/media/css/get-started.css b/obp-api/src/main/webapp/media/css/get-started.css index 45ab63d64..3e22c1ee7 100644 --- a/obp-api/src/main/webapp/media/css/get-started.css +++ b/obp-api/src/main/webapp/media/css/get-started.css @@ -28,13 +28,13 @@ margin-bottom: 17px; } #main-get-started .main-get-started-text p{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; } #main-get-started .main-get-started-text p a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -64,7 +64,7 @@ } #main-get-started .btn-primary a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; text-align: center; diff --git a/obp-api/src/main/webapp/media/css/main-apis.css b/obp-api/src/main/webapp/media/css/main-apis.css index 1270314f6..db29c59f4 100644 --- a/obp-api/src/main/webapp/media/css/main-apis.css +++ b/obp-api/src/main/webapp/media/css/main-apis.css @@ -30,7 +30,7 @@ margin-left: 7.2px; } #main-apis .main-apis-text p{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -59,7 +59,7 @@ background-color: #EDEDED; } #main-apis .btn-secondary a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; text-align: center; diff --git a/obp-api/src/main/webapp/media/css/main-faq.css b/obp-api/src/main/webapp/media/css/main-faq.css index cb07ec399..b2651a37b 100644 --- a/obp-api/src/main/webapp/media/css/main-faq.css +++ b/obp-api/src/main/webapp/media/css/main-faq.css @@ -17,7 +17,7 @@ } #main-faq h3 { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -44,7 +44,7 @@ #main-faq div a{ margin-top: 10px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -57,7 +57,7 @@ padding: 0; border: 1px solid transparent; background-color: #FFFFFF; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/main-showcases.css b/obp-api/src/main/webapp/media/css/main-showcases.css index 06e5122c6..27b4fa9cc 100644 --- a/obp-api/src/main/webapp/media/css/main-showcases.css +++ b/obp-api/src/main/webapp/media/css/main-showcases.css @@ -16,7 +16,7 @@ display: inline-block; margin: 30px 40px; width: 300px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; } @@ -24,7 +24,7 @@ #main-showcases p { color: white; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; line-height: 24px; } diff --git a/obp-api/src/main/webapp/media/css/main-start.css b/obp-api/src/main/webapp/media/css/main-start.css index 474ff9ea2..37ab3844c 100644 --- a/obp-api/src/main/webapp/media/css/main-start.css +++ b/obp-api/src/main/webapp/media/css/main-start.css @@ -28,7 +28,7 @@ padding: 64px 0; } #main-start #main-start_building .btn-default a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; text-align: center; diff --git a/obp-api/src/main/webapp/media/css/main-support.css b/obp-api/src/main/webapp/media/css/main-support.css index 39b2b4b9e..8652c2db5 100644 --- a/obp-api/src/main/webapp/media/css/main-support.css +++ b/obp-api/src/main/webapp/media/css/main-support.css @@ -14,7 +14,7 @@ margin-bottom: 9px ; } .main-support-item a { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/nav.css b/obp-api/src/main/webapp/media/css/nav.css index 420a0707e..c06a92139 100644 --- a/obp-api/src/main/webapp/media/css/nav.css +++ b/obp-api/src/main/webapp/media/css/nav.css @@ -53,7 +53,7 @@ nav .navbar-collapse.collapse { margin-right: 0; } .navbar-default .navbar-right .navitem p #register-link { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -79,7 +79,7 @@ nav .navbar-collapse.collapse { .navbar-default .navbar-right .navbar-btn #loggedIn-username { margin-right: 24px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -130,7 +130,7 @@ nav .navbar-collapse.collapse { } #small-screen-navbar #small-nav-log-on-button .login{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 12px; color: #FFFFFF; letter-spacing: 0; @@ -157,7 +157,7 @@ nav .navbar-collapse.collapse { .sidebar a { text-decoration: none; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/obp-toastr.css b/obp-api/src/main/webapp/media/css/obp-toastr.css index e167758ef..0b2769033 100644 --- a/obp-api/src/main/webapp/media/css/obp-toastr.css +++ b/obp-api/src/main/webapp/media/css/obp-toastr.css @@ -43,7 +43,7 @@ } #toast-container .toast-message { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/recover-password.css b/obp-api/src/main/webapp/media/css/recover-password.css index bf24d8a7f..9fc17b968 100644 --- a/obp-api/src/main/webapp/media/css/recover-password.css +++ b/obp-api/src/main/webapp/media/css/recover-password.css @@ -23,7 +23,7 @@ border: 0; height: 44px; width: 92px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; text-align: center; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/register-consumer.css b/obp-api/src/main/webapp/media/css/register-consumer.css index c4f5dafa4..7268d9ef1 100644 --- a/obp-api/src/main/webapp/media/css/register-consumer.css +++ b/obp-api/src/main/webapp/media/css/register-consumer.css @@ -40,7 +40,7 @@ #register-consumer #register-consumer-explanation p:last-child{ margin-top: 8px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; letter-spacing: 0; @@ -51,7 +51,7 @@ #register-consumer-input form label{ margin-bottom: 8px; margin-top: 17px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -114,7 +114,7 @@ #register-consumer-success span, #register-consumer-success a{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -144,7 +144,7 @@ #register-consumer-input #consumer-registration-app-description-error, #register-consumer-input #consumer-registration-app-developer-error, #register-consumer-input #register-consumer-errors{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; line-height: 20px; diff --git a/obp-api/src/main/webapp/media/css/signup.css b/obp-api/src/main/webapp/media/css/signup.css index 35f2bb396..60db8b229 100644 --- a/obp-api/src/main/webapp/media/css/signup.css +++ b/obp-api/src/main/webapp/media/css/signup.css @@ -19,7 +19,7 @@ } #signup form span{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -32,7 +32,7 @@ #signup form #repeat-password{ margin-top: 32px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -43,7 +43,7 @@ } #signup form label{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -138,7 +138,7 @@ font-size: 16px; } #signup-agree-terms #signup-agree-terms-content { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; letter-spacing: 0; @@ -151,7 +151,7 @@ #signup #signup-submit input { color: #FFF; background-color: #53C4EF; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; text-align: center; @@ -223,7 +223,7 @@ margin-bottom: 0px; } #signup-legal-notice{ - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 14px; color: #333333; letter-spacing: 0; diff --git a/obp-api/src/main/webapp/media/css/website.css b/obp-api/src/main/webapp/media/css/website.css index c4a621bd8..989894a8a 100644 --- a/obp-api/src/main/webapp/media/css/website.css +++ b/obp-api/src/main/webapp/media/css/website.css @@ -31,7 +31,7 @@ body { } a { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; color: #333333; text-decoration: none; } @@ -133,7 +133,7 @@ header #lift__noticesContainer__ { } .btn-danger { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #FFFFFF; text-align: center; @@ -164,7 +164,7 @@ header #lift__noticesContainer__ { padding-bottom: 11px; padding-left: 20px; padding-right: 20px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; text-align: center; @@ -213,7 +213,7 @@ input[type="text"]{ } .select2-results__option { padding: 6px; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -224,7 +224,7 @@ input[type="text"]{ } .select2-container--default .select2-selection--single .select2-selection__rendered { - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -428,7 +428,7 @@ input{ #confirm-user-auth-context-update-request-div #otp-value-error .error{ color: black; background-color: white; - font-family: Roboto-Regular; + font-family: Roboto-Regular,sans-serif; font-size: 16px; color: #333333; line-height: 24px; From 237a090e37adf6d48a576f9d9c72a37ff196df5d Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 11:53:44 +0100 Subject: [PATCH 158/185] bugfix/Unexpected missing generic font family - step2 --- .../src/main/webapp/media/css/authorise.css | 4 +-- .../src/main/webapp/media/css/data-area.css | 10 +++---- obp-api/src/main/webapp/media/css/fonts.css | 4 +-- .../src/main/webapp/media/css/main-faq.css | 2 +- obp-api/src/main/webapp/media/css/nav.css | 2 +- .../src/main/webapp/media/css/obp-toastr.css | 2 +- .../webapp/media/css/recover-password.css | 2 +- .../webapp/media/css/register-consumer.css | 12 ++++---- obp-api/src/main/webapp/media/css/reset.css | 2 +- .../src/main/webapp/media/css/responsive.css | 28 +++++++++---------- obp-api/src/main/webapp/media/css/signup.css | 2 +- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/obp-api/src/main/webapp/media/css/authorise.css b/obp-api/src/main/webapp/media/css/authorise.css index 714ac67e4..28fe6ef16 100644 --- a/obp-api/src/main/webapp/media/css/authorise.css +++ b/obp-api/src/main/webapp/media/css/authorise.css @@ -5,7 +5,7 @@ } #authorise h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; line-height: 36px; @@ -102,7 +102,7 @@ color: red; } #authorise #thanks-h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; letter-spacing: 0; diff --git a/obp-api/src/main/webapp/media/css/data-area.css b/obp-api/src/main/webapp/media/css/data-area.css index 82a0dc0cc..e729672e7 100644 --- a/obp-api/src/main/webapp/media/css/data-area.css +++ b/obp-api/src/main/webapp/media/css/data-area.css @@ -17,7 +17,7 @@ color: black; } #data-area h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; letter-spacing: 0; @@ -28,7 +28,7 @@ } #data-area #data-area-explanation p:nth-child(2) { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 22px; color: #333333; letter-spacing: 0; @@ -88,7 +88,7 @@ } #data-area #data-area-success #data-area-success-message{ - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 22px; color: #333333; letter-spacing: 0; @@ -98,7 +98,7 @@ #data-area #data-area-success p { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 22px; color: #333333; letter-spacing: 0; @@ -123,7 +123,7 @@ margin-bottom: 20px; } #data-area #data-area-success .row div:nth-child(1) { - font-family: Roboto-Medium; + font-family: Roboto-Medium,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/fonts.css b/obp-api/src/main/webapp/media/css/fonts.css index 3120bb343..3d35bb34a 100644 --- a/obp-api/src/main/webapp/media/css/fonts.css +++ b/obp-api/src/main/webapp/media/css/fonts.css @@ -1,12 +1,12 @@ /*https://fonts.google.com/specimen/Roboto?selection.family=Roboto&sidebar.open=true*/ /*We use the Roboto font as the OBP default font*/ @font-face { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; src: url(../font/Roboto-Light.ttf); } @font-face { - font-family: Roboto-Medium; + font-family: Roboto-Medium,sans-serif; src: url(../font/Roboto-Medium.ttf); } diff --git a/obp-api/src/main/webapp/media/css/main-faq.css b/obp-api/src/main/webapp/media/css/main-faq.css index b2651a37b..5a07aa450 100644 --- a/obp-api/src/main/webapp/media/css/main-faq.css +++ b/obp-api/src/main/webapp/media/css/main-faq.css @@ -35,7 +35,7 @@ } #main-faq p, #main-faq ol{ margin-top: 18px; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/nav.css b/obp-api/src/main/webapp/media/css/nav.css index c06a92139..435129e14 100644 --- a/obp-api/src/main/webapp/media/css/nav.css +++ b/obp-api/src/main/webapp/media/css/nav.css @@ -25,7 +25,7 @@ nav .navbar-collapse.collapse { } .navbar-default .navbar-nav > li > a { background-color: white; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 18px; color: #333333; line-height: 31px; diff --git a/obp-api/src/main/webapp/media/css/obp-toastr.css b/obp-api/src/main/webapp/media/css/obp-toastr.css index 0b2769033..79f73a5db 100644 --- a/obp-api/src/main/webapp/media/css/obp-toastr.css +++ b/obp-api/src/main/webapp/media/css/obp-toastr.css @@ -26,7 +26,7 @@ #toast-container .toast-title { float: left; font-weight: bold; - font-family: Roboto-Medium; + font-family: Roboto-Medium,sans-serif; font-size: 16px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/recover-password.css b/obp-api/src/main/webapp/media/css/recover-password.css index 9fc17b968..1c37005d7 100644 --- a/obp-api/src/main/webapp/media/css/recover-password.css +++ b/obp-api/src/main/webapp/media/css/recover-password.css @@ -1,6 +1,6 @@ #recover-password h1 { margin-top: 52px; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 35px; color: #333333; text-align: center; diff --git a/obp-api/src/main/webapp/media/css/register-consumer.css b/obp-api/src/main/webapp/media/css/register-consumer.css index 7268d9ef1..476560036 100644 --- a/obp-api/src/main/webapp/media/css/register-consumer.css +++ b/obp-api/src/main/webapp/media/css/register-consumer.css @@ -17,7 +17,7 @@ color: black; } #register-consumer h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; letter-spacing: 0; @@ -28,7 +28,7 @@ } #register-consumer #register-consumer-explanation p:nth-child(2) { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 22px; color: #333333; letter-spacing: 0; @@ -88,7 +88,7 @@ } #register-consumer #register-consumer-success #register-consumer-success-message{ - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 22px; color: #333333; letter-spacing: 0; @@ -98,7 +98,7 @@ #register-consumer #register-consumer-success p { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 22px; color: #333333; letter-spacing: 0; @@ -123,7 +123,7 @@ margin-bottom: 20px; } #register-consumer #register-consumer-success .row div:nth-child(1) { - font-family: Roboto-Medium; + font-family: Roboto-Medium,sans-serif; font-size: 16px; color: #333333; line-height: 24px; @@ -157,7 +157,7 @@ } #dummy-user-tokens .col-xs-12:last-child{ - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 20px; color: #333333; line-height: 24px; diff --git a/obp-api/src/main/webapp/media/css/reset.css b/obp-api/src/main/webapp/media/css/reset.css index 1fb79bec2..805a1106a 100644 --- a/obp-api/src/main/webapp/media/css/reset.css +++ b/obp-api/src/main/webapp/media/css/reset.css @@ -22,7 +22,7 @@ time, mark, audio, video { font: inherit; font-size: 100%; vertical-align: baseline; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, diff --git a/obp-api/src/main/webapp/media/css/responsive.css b/obp-api/src/main/webapp/media/css/responsive.css index 9d5037105..bd94bc6cc 100644 --- a/obp-api/src/main/webapp/media/css/responsive.css +++ b/obp-api/src/main/webapp/media/css/responsive.css @@ -55,7 +55,7 @@ display: block; } h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 33px; line-height: 40px; color: #333333; @@ -64,21 +64,21 @@ } h2 { font-size: 28px; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; line-height: 36px; color: #333333; font-weight: lighter; text-align: center; } h3 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 23px; color: #333333; line-height: 30px; font-weight: lighter; } h4 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 19px; color: #333333; line-height: 27px; @@ -204,7 +204,7 @@ @media only screen and (min-width: 760px) and (max-width: 959px) { h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 33px; line-height: 40px; color: #333333; @@ -213,21 +213,21 @@ } h2 { font-size: 28px; - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; line-height: 36px; color: #333333; font-weight: lighter; text-align: center; } h3 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 23px; color: #333333; line-height: 15px; font-weight: lighter; } h4 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 19px; color: #333333; line-height: 27px; @@ -319,7 +319,7 @@ @media only screen and (min-width: 960px) and (max-width: 1279px) { h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 44px; color: #333333; letter-spacing: 0; @@ -327,7 +327,7 @@ } h2 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 35px; color: #333333; text-align: center; @@ -335,7 +335,7 @@ } h3 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; line-height: 36px; @@ -387,7 +387,7 @@ @media only screen and (min-width: 1280px) { h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 44px; color: #333333; letter-spacing: 0; @@ -395,7 +395,7 @@ } h2 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 35px; color: #333333; text-align: center; @@ -403,7 +403,7 @@ } h3 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; line-height: 36px; diff --git a/obp-api/src/main/webapp/media/css/signup.css b/obp-api/src/main/webapp/media/css/signup.css index 60db8b229..91d6e5bdb 100644 --- a/obp-api/src/main/webapp/media/css/signup.css +++ b/obp-api/src/main/webapp/media/css/signup.css @@ -10,7 +10,7 @@ } #signup form h1 { - font-family: Roboto-Light; + font-family: Roboto-Light,sans-serif; font-size: 28px; color: #333333; letter-spacing: 0; From 9fc8ce894f4094dd9bb5d848dbca376fd6252ca0 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 13:57:01 +0100 Subject: [PATCH 159/185] bugfix/Correct one of the identical sub-expressions on both sides this operator --- obp-api/src/main/scala/code/atms/Atms.scala | 2 +- .../vJune2017/KafkaMappedConnector_vJune2017.scala | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/atms/Atms.scala b/obp-api/src/main/scala/code/atms/Atms.scala index 8c5f6ac4a..dede15ea6 100644 --- a/obp-api/src/main/scala/code/atms/Atms.scala +++ b/obp-api/src/main/scala/code/atms/Atms.scala @@ -92,7 +92,7 @@ trait AtmsProvider { getAtmsFromProvider(bankId,queryParams) match { case Some(atms) => { val atmsWithLicense = for { - branch <- atms if branch.meta.license.name.size > 3 && branch.meta.license.name.size > 3 + branch <- atms if branch.meta.license.name.size > 3 } yield branch Option(atmsWithLicense) } diff --git a/obp-api/src/main/scala/code/bankconnectors/vJune2017/KafkaMappedConnector_vJune2017.scala b/obp-api/src/main/scala/code/bankconnectors/vJune2017/KafkaMappedConnector_vJune2017.scala index 76ccbec7a..7a2cba025 100644 --- a/obp-api/src/main/scala/code/bankconnectors/vJune2017/KafkaMappedConnector_vJune2017.scala +++ b/obp-api/src/main/scala/code/bankconnectors/vJune2017/KafkaMappedConnector_vJune2017.scala @@ -1037,8 +1037,6 @@ trait KafkaMappedConnector_vJune2017 extends Connector with KafkaHelper with Mdc } yield { (transaction,callContext) } - case Full((data,status,callContext)) if (status.errorCode!="") => - Failure("INTERNAL-"+ status.errorCode+". + CoreBank-Status:"+ status.backendMessages) case Empty => Failure(ErrorMessages.InvalidConnectorResponse, Empty, Empty) case Failure(msg, e, c) => From 0bcdf6f88f1a470a184dd0047da5ffd8b34c3b19 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 14:00:58 +0100 Subject: [PATCH 160/185] refactor/comment the unused code --- .../examplething/MappedThingProvider.scala | 104 +++++------ .../main/scala/code/examplething/Thing.scala | 164 +++++++++--------- 2 files changed, 134 insertions(+), 134 deletions(-) diff --git a/obp-api/src/main/scala/code/examplething/MappedThingProvider.scala b/obp-api/src/main/scala/code/examplething/MappedThingProvider.scala index e4d9e7d68..53e1d0d7c 100644 --- a/obp-api/src/main/scala/code/examplething/MappedThingProvider.scala +++ b/obp-api/src/main/scala/code/examplething/MappedThingProvider.scala @@ -1,52 +1,52 @@ -package code.examplething - - -import code.util.UUIDString -import com.openbankproject.commons.model.BankId -import net.liftweb.common.Box -import net.liftweb.mapper._ - - - -object MappedThingProvider extends ThingProvider { - - override protected def getThingFromProvider(thingId: ThingId): Option[Thing] = - MappedThing.find(By(MappedThing.thingId_, thingId.value)) - - override protected def getThingsFromProvider(bankId: BankId): Option[List[Thing]] = { - Some(MappedThing.findAll(By(MappedThing.bankId_, bankId.value))) - } -} - -class MappedThing extends Thing with LongKeyedMapper[MappedThing] with IdPK { - - override def getSingleton = MappedThing - - object bankId_ extends UUIDString(this) - object name_ extends MappedString(this, 255) - - object thingId_ extends MappedString(this, 30) - - object fooSomething_ extends MappedString(this, 255) - object barSomething_ extends MappedString(this, 255) - - override def thingId: ThingId = ThingId(thingId_.get) - override def something: String = name_.get - - - override def foo: Foo = new Foo { - override def fooSomething: String = fooSomething_.get - } - - override def bar: Bar = new Bar { - override def barSomething: String = barSomething_.get - } - - -} - - -object MappedThing extends MappedThing with LongKeyedMetaMapper[MappedThing] { - override def dbIndexes = UniqueIndex(bankId_, thingId_) :: Index(bankId_) :: super.dbIndexes -} - +//package code.examplething +// +// +//import code.util.UUIDString +//import com.openbankproject.commons.model.BankId +//import net.liftweb.common.Box +//import net.liftweb.mapper._ +// +// +// +//object MappedThingProvider extends ThingProvider { +// +// override protected def getThingFromProvider(thingId: ThingId): Option[Thing] = +// MappedThing.find(By(MappedThing.thingId_, thingId.value)) +// +// override protected def getThingsFromProvider(bankId: BankId): Option[List[Thing]] = { +// Some(MappedThing.findAll(By(MappedThing.bankId_, bankId.value))) +// } +//} +// +//class MappedThing extends Thing with LongKeyedMapper[MappedThing] with IdPK { +// +// override def getSingleton = MappedThing +// +// object bankId_ extends UUIDString(this) +// object name_ extends MappedString(this, 255) +// +// object thingId_ extends MappedString(this, 30) +// +// object fooSomething_ extends MappedString(this, 255) +// object barSomething_ extends MappedString(this, 255) +// +// override def thingId: ThingId = ThingId(thingId_.get) +// override def something: String = name_.get +// +// +// override def foo: Foo = new Foo { +// override def fooSomething: String = fooSomething_.get +// } +// +// override def bar: Bar = new Bar { +// override def barSomething: String = barSomething_.get +// } +// +// +//} +// +// +//object MappedThing extends MappedThing with LongKeyedMetaMapper[MappedThing] { +// override def dbIndexes = UniqueIndex(bankId_, thingId_) :: Index(bankId_) :: super.dbIndexes +//} +// diff --git a/obp-api/src/main/scala/code/examplething/Thing.scala b/obp-api/src/main/scala/code/examplething/Thing.scala index 870928099..64800baa9 100644 --- a/obp-api/src/main/scala/code/examplething/Thing.scala +++ b/obp-api/src/main/scala/code/examplething/Thing.scala @@ -1,82 +1,82 @@ -package code.examplething - - -// Need to import these one by one because in same package! -import code.api.util.APIUtil -import com.openbankproject.commons.model.BankId -import net.liftweb.common.Logger -import net.liftweb.util.SimpleInjector - -object Thing extends SimpleInjector { - - val thingProvider = new Inject(buildOne _) {} - // def buildOne: ThingProvider = MappedThingProvider - - - // This determines the provider we use - def buildOne: ThingProvider = - APIUtil.getPropsValue("provider.thing").openOr("mapped") match { - case "mapped" => MappedThingProvider - case _ => MappedThingProvider - } - -} - -case class ThingId(value : String) - -trait Thing { - def thingId : ThingId - def something : String - def foo : Foo - def bar : Bar -} - -trait Foo { - def fooSomething : String -} - -trait Bar { - def barSomething : String -} - - -/* -A trait that defines interfaces to Thing -i.e. a ThingProvider should provide these: - */ - -trait ThingProvider { - - private val logger = Logger(classOf[ThingProvider]) - - - /* - Common logic for returning or changing Things - Datasource implementation details are in Thing provider - */ - final def getThings(bankId : BankId) : Option[List[Thing]] = { - getThingsFromProvider(bankId) match { - case Some(things) => { - - val certainThings = for { - thing <- things // if thing.meta.license.name.size > 3 - } yield thing - Option(certainThings) - } - case None => None - } - } - - /* - Return one Thing - */ - final def getThing(thingId : ThingId) : Option[Thing] = { - // Could do something here - getThingFromProvider(thingId) //.filter... - } - - protected def getThingFromProvider(thingId : ThingId) : Option[Thing] - protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]] - -} - +//package code.examplething +// +// +//// Need to import these one by one because in same package! +//import code.api.util.APIUtil +//import com.openbankproject.commons.model.BankId +//import net.liftweb.common.Logger +//import net.liftweb.util.SimpleInjector +// +//object Thing extends SimpleInjector { +// +// val thingProvider = new Inject(buildOne _) {} +// // def buildOne: ThingProvider = MappedThingProvider +// +// +// // This determines the provider we use +// def buildOne: ThingProvider = +// APIUtil.getPropsValue("provider.thing").openOr("mapped") match { +// case "mapped" => MappedThingProvider +// case _ => MappedThingProvider +// } +// +//} +// +//case class ThingId(value : String) +// +//trait Thing { +// def thingId : ThingId +// def something : String +// def foo : Foo +// def bar : Bar +//} +// +//trait Foo { +// def fooSomething : String +//} +// +//trait Bar { +// def barSomething : String +//} +// +// +///* +//A trait that defines interfaces to Thing +//i.e. a ThingProvider should provide these: +// */ +// +//trait ThingProvider { +// +// private val logger = Logger(classOf[ThingProvider]) +// +// +// /* +// Common logic for returning or changing Things +// Datasource implementation details are in Thing provider +// */ +// final def getThings(bankId : BankId) : Option[List[Thing]] = { +// getThingsFromProvider(bankId) match { +// case Some(things) => { +// +// val certainThings = for { +// thing <- things // if thing.meta.license.name.size > 3 +// } yield thing +// Option(certainThings) +// } +// case None => None +// } +// } +// +// /* +// Return one Thing +// */ +// final def getThing(thingId : ThingId) : Option[Thing] = { +// // Could do something here +// getThingFromProvider(thingId) //.filter... +// } +// +// protected def getThingFromProvider(thingId : ThingId) : Option[Thing] +// protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]] +// +//} +// From 5cd42d022f36580fe49b944bac53c9b2e5ceaf8d Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 14:01:57 +0100 Subject: [PATCH 161/185] feature/Correct one of the identical sub-expressions on both sides this operator --- obp-api/src/main/scala/code/products/Products.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/products/Products.scala b/obp-api/src/main/scala/code/products/Products.scala index e902f8f1d..cef006aa2 100644 --- a/obp-api/src/main/scala/code/products/Products.scala +++ b/obp-api/src/main/scala/code/products/Products.scala @@ -43,7 +43,7 @@ trait ProductsProvider { case Some(products) => { val productsWithLicense = for { // Only return products that have a license set unless its for an admin view - product <- products if (adminView || (product.meta.license.name.size > 3 && product.meta.license.name.size > 3)) + product <- products if (adminView || (product.meta.license.name.size > 3)) } yield product Option(productsWithLicense) } From 9337ad1cb84253fe4b085b1abf64164b71d88c2e Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 14:06:27 +0100 Subject: [PATCH 162/185] refactor/commented YearlyCharge code --- .../MappedYearlyChargeProvider.scala | 152 +++++++++--------- .../yearlycustomercharges/YearlyCharge.scala | 142 ++++++++-------- 2 files changed, 147 insertions(+), 147 deletions(-) diff --git a/obp-api/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala b/obp-api/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala index 8e47ba47c..592d9997d 100644 --- a/obp-api/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala +++ b/obp-api/src/main/scala/code/yearlycustomercharges/MappedYearlyChargeProvider.scala @@ -1,76 +1,76 @@ -package code.yearlycustomercharges - -import code.util.UUIDString -import com.openbankproject.commons.model.{BankId, CustomerId} -import net.liftweb.common.Box -import net.liftweb.mapper._ - - - -object MappedYearlyChargeProvider extends YearlyChargeProvider { - -// override protected def getYearlyChargeFromProvider(thingId: YearlyChargeId): Option[YearlyCharge] = -// MappedYearlyCharge.find(By(MappedYearlyCharge.thingId_, thingId.value)) - - override protected def getYearlyChargesFromProvider(bankId: BankId, customerId: CustomerId, year: Int): Option[List[YearlyCharge]] = { - Some(MappedYearlyCharge.findAll(By(MappedYearlyCharge.bankId_, bankId.value), By(MappedYearlyCharge.customerId_, customerId.value))) - } -} - -class MappedYearlyCharge extends YearlyCharge with LongKeyedMapper[MappedYearlyCharge] with IdPK { - - override def getSingleton = MappedYearlyCharge - - object bankId_ extends UUIDString(this) - object customerId_ extends UUIDString(this) - - object year_ extends MappedInt(this) - - - - //override def yearlyChargeId: YearlyChargeId = YearlyChargeId(id.get) - override def year: Int = year_.get - - - // override def getSingleton = MappedYearlyCustomerCharge - // - // WIP - // object mCustomerNumber extends MappedString(this,123) - // - // object mYear extends MappedInt(this) - // - // object mCategoryId extends UUIDString(this) - // object mForcastIndictor extends MappedString(this,123) - // object mTypeId extends MappedString(this,123) - // object mNatureId extends UUIDString(this) - // - // - // object mCharge_Currency extends MappedString(this,3) - // object mCharge_Amount extends MappedString(this,32) - // - // object mUpdateDate extends MappedDateTime(this) - // - // - // //override def bankId: String = mBankId.get - // override def customerId: String = mCustomerId.get // id.toString - // override def customerNumber: String = mCustomerNumber.get - // override def year: Integer = mYear.get - // - // override def categoryId: String = mCategoryId.get - // override def forcastIndictor: String = mForcastIndictor.get - // override def typeId: String = mTypeId.get - // override def natureId : String = mNatureId.get - // - // override def charge: AmountOfMoney = AmountOfMoney(mCharge_Currency.get, mCharge_Amount.get) - // override def updateDate : Date = mUpdateDate.get - - - - -} - - -object MappedYearlyCharge extends MappedYearlyCharge with LongKeyedMetaMapper[MappedYearlyCharge] { - override def dbIndexes = UniqueIndex(bankId_, customerId_, year_) :: Index(bankId_) :: super.dbIndexes -} - +//package code.yearlycustomercharges +// +//import code.util.UUIDString +//import com.openbankproject.commons.model.{BankId, CustomerId} +//import net.liftweb.common.Box +//import net.liftweb.mapper._ +// +// +// +//object MappedYearlyChargeProvider extends YearlyChargeProvider { +// +//// override protected def getYearlyChargeFromProvider(thingId: YearlyChargeId): Option[YearlyCharge] = +//// MappedYearlyCharge.find(By(MappedYearlyCharge.thingId_, thingId.value)) +// +// override protected def getYearlyChargesFromProvider(bankId: BankId, customerId: CustomerId, year: Int): Option[List[YearlyCharge]] = { +// Some(MappedYearlyCharge.findAll(By(MappedYearlyCharge.bankId_, bankId.value), By(MappedYearlyCharge.customerId_, customerId.value))) +// } +//} +// +//class MappedYearlyCharge extends YearlyCharge with LongKeyedMapper[MappedYearlyCharge] with IdPK { +// +// override def getSingleton = MappedYearlyCharge +// +// object bankId_ extends UUIDString(this) +// object customerId_ extends UUIDString(this) +// +// object year_ extends MappedInt(this) +// +// +// +// //override def yearlyChargeId: YearlyChargeId = YearlyChargeId(id.get) +// override def year: Int = year_.get +// +// +// // override def getSingleton = MappedYearlyCustomerCharge +// // +// // WIP +// // object mCustomerNumber extends MappedString(this,123) +// // +// // object mYear extends MappedInt(this) +// // +// // object mCategoryId extends UUIDString(this) +// // object mForcastIndictor extends MappedString(this,123) +// // object mTypeId extends MappedString(this,123) +// // object mNatureId extends UUIDString(this) +// // +// // +// // object mCharge_Currency extends MappedString(this,3) +// // object mCharge_Amount extends MappedString(this,32) +// // +// // object mUpdateDate extends MappedDateTime(this) +// // +// // +// // //override def bankId: String = mBankId.get +// // override def customerId: String = mCustomerId.get // id.toString +// // override def customerNumber: String = mCustomerNumber.get +// // override def year: Integer = mYear.get +// // +// // override def categoryId: String = mCategoryId.get +// // override def forcastIndictor: String = mForcastIndictor.get +// // override def typeId: String = mTypeId.get +// // override def natureId : String = mNatureId.get +// // +// // override def charge: AmountOfMoney = AmountOfMoney(mCharge_Currency.get, mCharge_Amount.get) +// // override def updateDate : Date = mUpdateDate.get +// +// +// +// +//} +// +// +//object MappedYearlyCharge extends MappedYearlyCharge with LongKeyedMetaMapper[MappedYearlyCharge] { +// override def dbIndexes = UniqueIndex(bankId_, customerId_, year_) :: Index(bankId_) :: super.dbIndexes +//} +// diff --git a/obp-api/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala b/obp-api/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala index 986b81e42..92d55fe54 100644 --- a/obp-api/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala +++ b/obp-api/src/main/scala/code/yearlycustomercharges/YearlyCharge.scala @@ -1,71 +1,71 @@ -package code.yearlycustomercharges - -import code.api.util.APIUtil -import com.openbankproject.commons.model.{BankId, CustomerId} -import net.liftweb.common.Logger -import net.liftweb.util.SimpleInjector - -object YearlyCharge extends SimpleInjector { - - val yearlyChargeProvider = new Inject(buildOne _) {} - - - // This determines the provider we use - def buildOne: YearlyChargeProvider = - APIUtil.getPropsValue("provider.thing").openOr("mapped") match { - case "mapped" => MappedYearlyChargeProvider - case _ => MappedYearlyChargeProvider - } - -} - -case class YearlyChargeId(value : String) - -// WIP -trait YearlyCharge { - def year : Int - - // def customerNumber : String - // - // - // def categoryId : String - // def forcastIndictor : String - // def typeId : String - // def natureId : String - // def charge : AmountOfMoney - // def updateDate : Date - - - -} - - - - -trait YearlyChargeProvider { - - private val logger = Logger(classOf[YearlyChargeProvider]) - - - /* - Common logic for returning or changing Things - Datasource implementation details are in Thing provider - */ - final def getYearlyCharges(bankId : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] = { - getYearlyChargesFromProvider(bankId, customerId, year) match { - case Some(things) => { - - val certainThings = for { - thing <- things // if filter etc. if need be - } yield thing - Option(certainThings) - } - case None => None - } - } - - - protected def getYearlyChargesFromProvider(bank : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] - -} - +//package code.yearlycustomercharges +// +//import code.api.util.APIUtil +//import com.openbankproject.commons.model.{BankId, CustomerId} +//import net.liftweb.common.Logger +//import net.liftweb.util.SimpleInjector +// +//object YearlyCharge extends SimpleInjector { +// +// val yearlyChargeProvider = new Inject(buildOne _) {} +// +// +// // This determines the provider we use +// def buildOne: YearlyChargeProvider = +// APIUtil.getPropsValue("provider.thing").openOr("mapped") match { +// case "mapped" => MappedYearlyChargeProvider +// case _ => MappedYearlyChargeProvider +// } +// +//} +// +//case class YearlyChargeId(value : String) +// +//// WIP +//trait YearlyCharge { +// def year : Int +// +// // def customerNumber : String +// // +// // +// // def categoryId : String +// // def forcastIndictor : String +// // def typeId : String +// // def natureId : String +// // def charge : AmountOfMoney +// // def updateDate : Date +// +// +// +//} +// +// +// +// +//trait YearlyChargeProvider { +// +// private val logger = Logger(classOf[YearlyChargeProvider]) +// +// +// /* +// Common logic for returning or changing Things +// Datasource implementation details are in Thing provider +// */ +// final def getYearlyCharges(bankId : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] = { +// getYearlyChargesFromProvider(bankId, customerId, year) match { +// case Some(things) => { +// +// val certainThings = for { +// thing <- things // if filter etc. if need be +// } yield thing +// Option(certainThings) +// } +// case None => None +// } +// } +// +// +// protected def getYearlyChargesFromProvider(bank : BankId, customerId: CustomerId, year: Int) : Option[List[YearlyCharge]] +// +//} +// From 70b684275dc61d05bb34d72f4920855602ec8834 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 14:07:33 +0100 Subject: [PATCH 163/185] refactor/Unexpected duplicate "height" --- obp-api/src/main/webapp/media/css/recover-password.css | 1 - obp-api/src/main/webapp/media/css/signup.css | 1 - 2 files changed, 2 deletions(-) diff --git a/obp-api/src/main/webapp/media/css/recover-password.css b/obp-api/src/main/webapp/media/css/recover-password.css index 1c37005d7..d3ba071c8 100644 --- a/obp-api/src/main/webapp/media/css/recover-password.css +++ b/obp-api/src/main/webapp/media/css/recover-password.css @@ -50,7 +50,6 @@ margin-top: 20px; display: block; width: 100%; - height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; diff --git a/obp-api/src/main/webapp/media/css/signup.css b/obp-api/src/main/webapp/media/css/signup.css index 91d6e5bdb..9ecba169b 100644 --- a/obp-api/src/main/webapp/media/css/signup.css +++ b/obp-api/src/main/webapp/media/css/signup.css @@ -149,7 +149,6 @@ } #signup #signup-submit input { - color: #FFF; background-color: #53C4EF; font-family: Roboto-Regular,sans-serif; font-size: 16px; From b2a898078971dfcd4cb0b0ee804a6f983b9ffecf Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 14:09:31 +0100 Subject: [PATCH 164/185] bugfix/Unexpected missing generic font family --- obp-api/src/main/webapp/media/css/website.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/webapp/media/css/website.css b/obp-api/src/main/webapp/media/css/website.css index 989894a8a..7ab7b0fcc 100644 --- a/obp-api/src/main/webapp/media/css/website.css +++ b/obp-api/src/main/webapp/media/css/website.css @@ -19,7 +19,7 @@ @import url(/media/css/fonts.css); html { - font-family: "Roboto-Regular"; + font-family: Roboto-Regular,sans-serif; height: 100% } From 76af23c2cb72ff9f7d2dbe3fc2429fe7ab73186a Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 14:12:58 +0100 Subject: [PATCH 165/185] refactor/fixed the bugs for sonarcloud --- obp-api/src/main/webapp/media/css/website.css | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/obp-api/src/main/webapp/media/css/website.css b/obp-api/src/main/webapp/media/css/website.css index 7ab7b0fcc..8eace0157 100644 --- a/obp-api/src/main/webapp/media/css/website.css +++ b/obp-api/src/main/webapp/media/css/website.css @@ -165,7 +165,6 @@ header #lift__noticesContainer__ { padding-left: 20px; padding-right: 20px; font-family: Roboto-Regular,sans-serif; - font-size: 16px; color: #333333; text-align: center; line-height: 24px; @@ -275,7 +274,7 @@ input[type="text"]{ } body { - font-family: "Roboto-Light"; + font-family: Roboto-Light,sans-serif; } header #header-decoration, @@ -300,7 +299,6 @@ header #header-decoration, min-width: 43%; padding: 6%; padding-top: 6%; - padding-right: 6%; padding-bottom: 6%; padding-left: 6%; padding-right: 10%; @@ -426,7 +424,6 @@ input{ #add-user-auth-context-update-request-div #identifier-error .error, #confirm-user-auth-context-update-request-div #otp-value-error .error{ - color: black; background-color: white; font-family: Roboto-Regular,sans-serif; font-size: 16px; From a59214296eb1c77410e54c6ac74e25d35c0ca56a Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 14:26:12 +0100 Subject: [PATCH 166/185] bugfix/fixed the bugs of sonarcloud --- obp-api/src/main/webapp/consumer-registration.html | 2 +- obp-api/src/main/webapp/templates-hidden/default.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/webapp/consumer-registration.html b/obp-api/src/main/webapp/consumer-registration.html index 0bdd9878a..d9e61aea1 100644 --- a/obp-api/src/main/webapp/consumer-registration.html +++ b/obp-api/src/main/webapp/consumer-registration.html @@ -137,7 +137,7 @@ Berlin 13359, Germany
- Content of jwks_uri. jwks_uri and jwks should not both have value at the same time. + Content of jwks_uri. jwks_uri and jwks should not both have value at the same time. Reference 10.1.1. Rotation of Asymmetric Signing Keys
diff --git a/obp-api/src/main/webapp/templates-hidden/default.html b/obp-api/src/main/webapp/templates-hidden/default.html index bc4df9ae9..0865cdbe1 100644 --- a/obp-api/src/main/webapp/templates-hidden/default.html +++ b/obp-api/src/main/webapp/templates-hidden/default.html @@ -70,7 +70,7 @@ Berlin 13359, Germany
- +
From ede56cbb3a66ca755fdafad56e424c092f4d3d81 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 14:55:18 +0100 Subject: [PATCH 167/185] bugfix/fixed the major bug of sonarcloud --- obp-api/src/main/webapp/plain.html | 2 ++ obp-api/src/main/webapp/templates-hidden/default.html | 4 +++- obp-api/src/test/scala/RunMTLSWebApp.scala | 2 +- obp-api/src/test/scala/RunWebApp.scala | 2 +- .../scala/com/openbankproject/commons/util/ReflectUtils.scala | 4 ++-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/webapp/plain.html b/obp-api/src/main/webapp/plain.html index 657cec28b..9394bb840 100644 --- a/obp-api/src/main/webapp/plain.html +++ b/obp-api/src/main/webapp/plain.html @@ -1,3 +1,5 @@ + + Example HTML diff --git a/obp-api/src/main/webapp/templates-hidden/default.html b/obp-api/src/main/webapp/templates-hidden/default.html index 0865cdbe1..cf34d6536 100644 --- a/obp-api/src/main/webapp/templates-hidden/default.html +++ b/obp-api/src/main/webapp/templates-hidden/default.html @@ -72,7 +72,9 @@ Berlin 13359, Germany
- + +
+
left logo image diff --git a/obp-api/src/test/scala/RunMTLSWebApp.scala b/obp-api/src/test/scala/RunMTLSWebApp.scala index 93a50ed58..edc181489 100644 --- a/obp-api/src/test/scala/RunMTLSWebApp.scala +++ b/obp-api/src/test/scala/RunMTLSWebApp.scala @@ -58,7 +58,7 @@ object RunMTLSWebApp extends App with PropsProgrammatically { servletContextPath } else { throw new IllegalAccessException(s"Should not call this object method except 'path' method, current call method name is: ${method.getName}") - ??? // should not call other method. +// ??? // should not call other method. } }).asInstanceOf[HTTPContext] LiftRules.setContext(tempHTTPContext) diff --git a/obp-api/src/test/scala/RunWebApp.scala b/obp-api/src/test/scala/RunWebApp.scala index 791c1ceb3..7f2484a77 100644 --- a/obp-api/src/test/scala/RunWebApp.scala +++ b/obp-api/src/test/scala/RunWebApp.scala @@ -51,7 +51,7 @@ object RunWebApp extends App { servletContextPath } else { throw new IllegalAccessException(s"Should not call this object method except 'path' method, current call method name is: ${method.getName}") - ??? // should not call other method. +// ??? // should not call other method. } }).asInstanceOf[HTTPContext] LiftRules.setContext(tempHTTPContext) diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala index bda7df656..dc32cad3f 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala @@ -45,7 +45,7 @@ object ReflectUtils { def isFieldOrCallByPath(term: ru.TermSymbol) = { term.name.decodedName.toString.trim == fieldName && - (term.isVal || term.isVal || term.isLazy || (term.isMethod && term.asMethod.paramLists.isEmpty)) + (term.isVal || term.isLazy || (term.isMethod && term.asMethod.paramLists.isEmpty)) } val fields: Iterable[ru.TermSymbol] = tp.members.collect({ @@ -495,7 +495,7 @@ object ReflectUtils { def findMethod(obj: Any, methodName: String)(predicate: Map[String, ru.Type] => Boolean): Option[MethodSymbol] = findMethod(getType(obj), methodName)(predicate) def findMethodByArgs(tp: ru.Type, methodName: String, args: Any*): Option[ru.MethodSymbol] = findMethod(tp, methodName) { nameToType => - args.size == args.size && nameToType.values.zip(args).forall(it => isTypeOf(it._1, it._2)) + nameToType.values.zip(args).forall(it => isTypeOf(it._1, it._2)) } def findMethodByArgs(obj: Any, methodName: String, args: Any*): Option[ru.MethodSymbol] = findMethodByArgs(getType(obj), methodName, args:_*) From cf0aef73a5e7d5b4c2c97890e5233a6f0def8a9e Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 15:08:24 +0100 Subject: [PATCH 168/185] bugfix/Add "lang" and/or "xml:lang" attributes to this "" element --- obp-api/src/main/webapp/plain.html | 1 - .../openbankproject/commons/util/RequiredFieldValidation.scala | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/obp-api/src/main/webapp/plain.html b/obp-api/src/main/webapp/plain.html index 9394bb840..8f1567748 100644 --- a/obp-api/src/main/webapp/plain.html +++ b/obp-api/src/main/webapp/plain.html @@ -1,6 +1,5 @@ - Example HTML

I am some Example HTML

diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/RequiredFieldValidation.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/RequiredFieldValidation.scala index 5d20bae5b..f531c9aaa 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/RequiredFieldValidation.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/RequiredFieldValidation.scala @@ -300,7 +300,7 @@ object RequiredFieldValidation { def getAnnotations(tp: Type): Iterable[RequiredArgs] = { def isField(symbol: TermSymbol): Boolean = - symbol.isVal || symbol.isVal || symbol.isLazy || (symbol.isMethod && symbol.asMethod.paramLists.isEmpty) + symbol.isVal || symbol.isLazy || (symbol.isMethod && symbol.asMethod.paramLists.isEmpty) // constructor's parameters and fields val members: Iterable[Symbol] = From 1763e0d11502f7c0704b09135932e23232aba558 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 15:36:28 +0100 Subject: [PATCH 169/185] feature/use select2.min.js instead of select2.js --- obp-api/src/main/webapp/media/js/select2.js | 6153 ----------------- .../src/main/webapp/media/js/select2.min.js | 1 + .../main/webapp/templates-hidden/default.html | 2 +- 3 files changed, 2 insertions(+), 6154 deletions(-) delete mode 100644 obp-api/src/main/webapp/media/js/select2.js create mode 100644 obp-api/src/main/webapp/media/js/select2.min.js diff --git a/obp-api/src/main/webapp/media/js/select2.js b/obp-api/src/main/webapp/media/js/select2.js deleted file mode 100644 index 7e1eabc58..000000000 --- a/obp-api/src/main/webapp/media/js/select2.js +++ /dev/null @@ -1,6153 +0,0 @@ -/*! - * Select2 4.1.0-beta.1 - * https://select2.github.io - * - * Released under the MIT license - * https://github.com/select2/select2/blob/master/LICENSE.md - */ -;(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else if (typeof module === 'object' && module.exports) { - // Node/CommonJS - module.exports = function (root, jQuery) { - if (jQuery === undefined) { - // require('jQuery') returns a factory that requires window to - // build a jQuery instance, we normalize how we use modules - // that require this pattern but the window provided is a noop - // if it's defined (how jquery works) - if (typeof window !== 'undefined') { - jQuery = require('jquery'); - } - else { - jQuery = require('jquery')(root); - } - } - factory(jQuery); - return jQuery; - }; - } else { - // Browser globals - factory(jQuery); - } -} (function (jQuery) { - // This is needed so we can catch the AMD loader configuration and use it - // The inner file should be wrapped (by `banner.start.js`) in a function that - // returns the AMD loader references. - var S2 =(function () { - // Restore the Select2 AMD loader so it can be used - // Needed mostly in the language files, where the loader is not inserted - if (jQuery && jQuery.fn && jQuery.fn.select2 && jQuery.fn.select2.amd) { - var S2 = jQuery.fn.select2.amd; - } - var S2;(function () { if (!S2 || !S2.requirejs) { - if (!S2) { S2 = {}; } else { require = S2; } - /** - * @license almond 0.3.3 Copyright jQuery Foundation and other contributors. - * Released under MIT license, http://github.com/requirejs/almond/LICENSE - */ -//Going sloppy to avoid 'use strict' string cost, but strict practices should -//be followed. - /*global setTimeout: false */ - - var requirejs, require, define; - (function (undef) { - var main, req, makeMap, handlers, - defined = {}, - waiting = {}, - config = {}, - defining = {}, - hasOwn = Object.prototype.hasOwnProperty, - aps = [].slice, - jsSuffixRegExp = /\.js$/; - - function hasProp(obj, prop) { - return hasOwn.call(obj, prop); - } - - /** - * Given a relative module name, like ./something, normalize it to - * a real name that can be mapped to a path. - * @param {String} name the relative name - * @param {String} baseName a real name that the name arg is relative - * to. - * @returns {String} normalized name - */ - function normalize(name, baseName) { - var nameParts, nameSegment, mapValue, foundMap, lastIndex, - foundI, foundStarMap, starI, i, j, part, normalizedBaseParts, - baseParts = baseName && baseName.split("/"), - map = config.map, - starMap = (map && map['*']) || {}; - - //Adjust any relative paths. - if (name) { - name = name.split('/'); - lastIndex = name.length - 1; - - // If wanting node ID compatibility, strip .js from end - // of IDs. Have to do this here, and not in nameToUrl - // because node allows either .js or non .js to map - // to same file. - if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { - name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); - } - - // Starts with a '.' so need the baseName - if (name[0].charAt(0) === '.' && baseParts) { - //Convert baseName to array, and lop off the last part, - //so that . matches that 'directory' and not name of the baseName's - //module. For instance, baseName of 'one/two/three', maps to - //'one/two/three.js', but we want the directory, 'one/two' for - //this normalization. - normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); - name = normalizedBaseParts.concat(name); - } - - //start trimDots - for (i = 0; i < name.length; i++) { - part = name[i]; - if (part === '.') { - name.splice(i, 1); - i -= 1; - } else if (part === '..') { - // If at the start, or previous value is still .., - // keep them so that when converted to a path it may - // still work when converted to a path, even though - // as an ID it is less than ideal. In larger point - // releases, may be better to just kick out an error. - if (i === 0 || (i === 1 && name[2] === '..') || name[i - 1] === '..') { - - } else if (i > 0) { - name.splice(i - 1, 2); - i -= 2; - } - } - } - //end trimDots - - name = name.join('/'); - } - - //Apply map config if available. - if ((baseParts || starMap) && map) { - nameParts = name.split('/'); - - for (i = nameParts.length; i > 0; i -= 1) { - nameSegment = nameParts.slice(0, i).join("/"); - - if (baseParts) { - //Find the longest baseName segment match in the config. - //So, do joins on the biggest to smallest lengths of baseParts. - for (j = baseParts.length; j > 0; j -= 1) { - mapValue = map[baseParts.slice(0, j).join('/')]; - - //baseName segment has config, find if it has one for - //this name. - if (mapValue) { - mapValue = mapValue[nameSegment]; - if (mapValue) { - //Match, update name to the new value. - foundMap = mapValue; - foundI = i; - break; - } - } - } - } - - if (foundMap) { - break; - } - - //Check for a star map match, but just hold on to it, - //if there is a shorter segment match later in a matching - //config, then favor over this star map. - if (!foundStarMap && starMap && starMap[nameSegment]) { - foundStarMap = starMap[nameSegment]; - starI = i; - } - } - - if (!foundMap && foundStarMap) { - foundMap = foundStarMap; - foundI = starI; - } - - if (foundMap) { - nameParts.splice(0, foundI, foundMap); - name = nameParts.join('/'); - } - } - - return name; - } - - function makeRequire(relName, forceSync) { - return function () { - //A version of a require function that passes a moduleName - //value for items that may need to - //look up paths relative to the moduleName - var args = aps.call(arguments, 0); - - //If first arg is not require('string'), and there is only - //one arg, it is the array form without a callback. Insert - //a null so that the following concat is correct. - if (typeof args[0] !== 'string' && args.length === 1) { - args.push(null); - } - return req.apply(undef, args.concat([relName, forceSync])); - }; - } - - function makeNormalize(relName) { - return function (name) { - return normalize(name, relName); - }; - } - - function makeLoad(depName) { - return function (value) { - defined[depName] = value; - }; - } - - function callDep(name) { - if (hasProp(waiting, name)) { - var args = waiting[name]; - delete waiting[name]; - defining[name] = true; - main.apply(undef, args); - } - - if (!hasProp(defined, name) && !hasProp(defining, name)) { - throw new Error('No ' + name); - } - return defined[name]; - } - - //Turns a plugin!resource to [plugin, resource] - //with the plugin being undefined if the name - //did not have a plugin prefix. - function splitPrefix(name) { - var prefix, - index = name ? name.indexOf('!') : -1; - if (index > -1) { - prefix = name.substring(0, index); - name = name.substring(index + 1, name.length); - } - return [prefix, name]; - } - - //Creates a parts array for a relName where first part is plugin ID, - //second part is resource ID. Assumes relName has already been normalized. - function makeRelParts(relName) { - return relName ? splitPrefix(relName) : []; - } - - /** - * Makes a name map, normalizing the name, and using a plugin - * for normalization if necessary. Grabs a ref to plugin - * too, as an optimization. - */ - makeMap = function (name, relParts) { - var plugin, - parts = splitPrefix(name), - prefix = parts[0], - relResourceName = relParts[1]; - - name = parts[1]; - - if (prefix) { - prefix = normalize(prefix, relResourceName); - plugin = callDep(prefix); - } - - //Normalize according - if (prefix) { - if (plugin && plugin.normalize) { - name = plugin.normalize(name, makeNormalize(relResourceName)); - } else { - name = normalize(name, relResourceName); - } - } else { - name = normalize(name, relResourceName); - parts = splitPrefix(name); - prefix = parts[0]; - name = parts[1]; - if (prefix) { - plugin = callDep(prefix); - } - } - - //Using ridiculous property names for space reasons - return { - f: prefix ? prefix + '!' + name : name, //fullName - n: name, - pr: prefix, - p: plugin - }; - }; - - function makeConfig(name) { - return function () { - return (config && config.config && config.config[name]) || {}; - }; - } - - handlers = { - require: function (name) { - return makeRequire(name); - }, - exports: function (name) { - var e = defined[name]; - if (typeof e !== 'undefined') { - return e; - } else { - return (defined[name] = {}); - } - }, - module: function (name) { - return { - id: name, - uri: '', - exports: defined[name], - config: makeConfig(name) - }; - } - }; - - main = function (name, deps, callback, relName) { - var cjsModule, depName, ret, map, i, relParts, - args = [], - callbackType = typeof callback, - usingExports; - - //Use name if no relName - relName = relName || name; - relParts = makeRelParts(relName); - - //Call the callback to define the module, if necessary. - if (callbackType === 'undefined' || callbackType === 'function') { - //Pull out the defined dependencies and pass the ordered - //values to the callback. - //Default to [require, exports, module] if no deps - deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; - for (i = 0; i < deps.length; i += 1) { - map = makeMap(deps[i], relParts); - depName = map.f; - - //Fast path CommonJS standard dependencies. - if (depName === "require") { - args[i] = handlers.require(name); - } else if (depName === "exports") { - //CommonJS module spec 1.1 - args[i] = handlers.exports(name); - usingExports = true; - } else if (depName === "module") { - //CommonJS module spec 1.1 - cjsModule = args[i] = handlers.module(name); - } else if (hasProp(defined, depName) || - hasProp(waiting, depName) || - hasProp(defining, depName)) { - args[i] = callDep(depName); - } else if (map.p) { - map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); - args[i] = defined[depName]; - } else { - throw new Error(name + ' missing ' + depName); - } - } - - ret = callback ? callback.apply(defined[name], args) : undefined; - - if (name) { - //If setting exports via "module" is in play, - //favor that over return value and exports. After that, - //favor a non-undefined return value over exports use. - if (cjsModule && cjsModule.exports !== undef && - cjsModule.exports !== defined[name]) { - defined[name] = cjsModule.exports; - } else if (ret !== undef || !usingExports) { - //Use the return value from the function. - defined[name] = ret; - } - } - } else if (name) { - //May just be an object definition for the module. Only - //worry about defining if have a module name. - defined[name] = callback; - } - }; - - requirejs = require = req = function (deps, callback, relName, forceSync, alt) { - if (typeof deps === "string") { - if (handlers[deps]) { - //callback in this case is really relName - return handlers[deps](callback); - } - //Just return the module wanted. In this scenario, the - //deps arg is the module name, and second arg (if passed) - //is just the relName. - //Normalize module name, if it contains . or .. - return callDep(makeMap(deps, makeRelParts(callback)).f); - } else if (!deps.splice) { - //deps is a config object, not an array. - config = deps; - if (config.deps) { - req(config.deps, config.callback); - } - if (!callback) { - return; - } - - if (callback.splice) { - //callback is an array, which means it is a dependency list. - //Adjust args if there are dependencies - deps = callback; - callback = relName; - relName = null; - } else { - deps = undef; - } - } - - //Support require(['a']) - callback = callback || function () {}; - - //If relName is a function, it is an errback handler, - //so remove it. - if (typeof relName === 'function') { - relName = forceSync; - forceSync = alt; - } - - //Simulate async callback; - if (forceSync) { - main(undef, deps, callback, relName); - } else { - //Using a non-zero value because of concern for what old browsers - //do, and latest browsers "upgrade" to 4 if lower value is used: - //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: - //If want a value immediately, use require('id') instead -- something - //that works in almond on the global level, but not guaranteed and - //unlikely to work in other AMD implementations. - setTimeout(function () { - main(undef, deps, callback, relName); - }, 4); - } - - return req; - }; - - /** - * Just drops the config on the floor, but returns req in case - * the config return value is used. - */ - req.config = function (cfg) { - return req(cfg); - }; - - /** - * Expose module registry for debugging and tooling - */ - requirejs._defined = defined; - - define = function (name, deps, callback) { - if (typeof name !== 'string') { - throw new Error('See almond README: incorrect module build, no module name'); - } - - //This module may not have dependencies - if (!deps.splice) { - //deps is not an array, so probably means - //an object literal or factory function for - //the value. Adjust args. - callback = deps; - deps = []; - } - - if (!hasProp(defined, name) && !hasProp(waiting, name)) { - waiting[name] = [name, deps, callback]; - } - }; - - define.amd = { - jQuery: true - }; - }()); - - S2.requirejs = requirejs;S2.require = require;S2.define = define; - } - }()); - S2.define("almond", function(){}); - - /* global jQuery:false, $:false */ - S2.define('jquery',[],function () { - var _$ = jQuery || $; - - if (_$ == null && console && console.error) { - console.error( - 'Select2: An instance of jQuery or a jQuery-compatible library was not ' + - 'found. Make sure that you are including jQuery before Select2 on your ' + - 'web page.' - ); - } - - return _$; - }); - - S2.define('select2/utils',[ - 'jquery' - ], function ($) { - var Utils = {}; - - Utils.Extend = function (ChildClass, SuperClass) { - var __hasProp = {}.hasOwnProperty; - - function BaseConstructor () { - this.constructor = ChildClass; - } - - for (var key in SuperClass) { - if (__hasProp.call(SuperClass, key)) { - ChildClass[key] = SuperClass[key]; - } - } - - BaseConstructor.prototype = SuperClass.prototype; - ChildClass.prototype = new BaseConstructor(); - ChildClass.__super__ = SuperClass.prototype; - - return ChildClass; - }; - - function getMethods (theClass) { - var proto = theClass.prototype; - - var methods = []; - - for (var methodName in proto) { - var m = proto[methodName]; - - if (typeof m !== 'function') { - continue; - } - - if (methodName === 'constructor') { - continue; - } - - methods.push(methodName); - } - - return methods; - } - - Utils.Decorate = function (SuperClass, DecoratorClass) { - var decoratedMethods = getMethods(DecoratorClass); - var superMethods = getMethods(SuperClass); - - function DecoratedClass () { - var unshift = Array.prototype.unshift; - - var argCount = DecoratorClass.prototype.constructor.length; - - var calledConstructor = SuperClass.prototype.constructor; - - if (argCount > 0) { - unshift.call(arguments, SuperClass.prototype.constructor); - - calledConstructor = DecoratorClass.prototype.constructor; - } - - calledConstructor.apply(this, arguments); - } - - DecoratorClass.displayName = SuperClass.displayName; - - function ctr () { - this.constructor = DecoratedClass; - } - - DecoratedClass.prototype = new ctr(); - - for (var m = 0; m < superMethods.length; m++) { - var superMethod = superMethods[m]; - - DecoratedClass.prototype[superMethod] = - SuperClass.prototype[superMethod]; - } - - var calledMethod = function (methodName) { - // Stub out the original method if it's not decorating an actual method - var originalMethod = function () {}; - - if (methodName in DecoratedClass.prototype) { - originalMethod = DecoratedClass.prototype[methodName]; - } - - var decoratedMethod = DecoratorClass.prototype[methodName]; - - return function () { - var unshift = Array.prototype.unshift; - - unshift.call(arguments, originalMethod); - - return decoratedMethod.apply(this, arguments); - }; - }; - - for (var d = 0; d < decoratedMethods.length; d++) { - var decoratedMethod = decoratedMethods[d]; - - DecoratedClass.prototype[decoratedMethod] = calledMethod(decoratedMethod); - } - - return DecoratedClass; - }; - - var Observable = function () { - this.listeners = {}; - }; - - Observable.prototype.on = function (event, callback) { - this.listeners = this.listeners || {}; - - if (event in this.listeners) { - this.listeners[event].push(callback); - } else { - this.listeners[event] = [callback]; - } - }; - - Observable.prototype.trigger = function (event) { - var slice = Array.prototype.slice; - var params = slice.call(arguments, 1); - - this.listeners = this.listeners || {}; - - // Params should always come in as an array - if (params == null) { - params = []; - } - - // If there are no arguments to the event, use a temporary object - if (params.length === 0) { - params.push({}); - } - - // Set the `_type` of the first object to the event - params[0]._type = event; - - if (event in this.listeners) { - this.invoke(this.listeners[event], slice.call(arguments, 1)); - } - - if ('*' in this.listeners) { - this.invoke(this.listeners['*'], arguments); - } - }; - - Observable.prototype.invoke = function (listeners, params) { - for (var i = 0, len = listeners.length; i < len; i++) { - listeners[i].apply(this, params); - } - }; - - Utils.Observable = Observable; - - Utils.generateChars = function (length) { - var chars = ''; - - for (var i = 0; i < length; i++) { - var randomChar = Math.floor(Math.random() * 36); - chars += randomChar.toString(36); - } - - return chars; - }; - - Utils.bind = function (func, context) { - return function () { - func.apply(context, arguments); - }; - }; - - Utils._convertData = function (data) { - for (var originalKey in data) { - var keys = originalKey.split('-'); - - var dataLevel = data; - - if (keys.length === 1) { - continue; - } - - for (var k = 0; k < keys.length; k++) { - var key = keys[k]; - - // Lowercase the first letter - // By default, dash-separated becomes camelCase - key = key.substring(0, 1).toLowerCase() + key.substring(1); - - if (!(key in dataLevel)) { - dataLevel[key] = {}; - } - - if (k == keys.length - 1) { - dataLevel[key] = data[originalKey]; - } - - dataLevel = dataLevel[key]; - } - - delete data[originalKey]; - } - - return data; - }; - - Utils.hasScroll = function (index, el) { - // Adapted from the function created by @ShadowScripter - // and adapted by @BillBarry on the Stack Exchange Code Review website. - // The original code can be found at - // http://codereview.stackexchange.com/q/13338 - // and was designed to be used with the Sizzle selector engine. - - var $el = $(el); - var overflowX = el.style.overflowX; - var overflowY = el.style.overflowY; - - //Check both x and y declarations - if (overflowX === overflowY && - (overflowY === 'hidden' || overflowY === 'visible')) { - return false; - } - - if (overflowX === 'scroll' || overflowY === 'scroll') { - return true; - } - - return ($el.innerHeight() < el.scrollHeight || - $el.innerWidth() < el.scrollWidth); - }; - - Utils.escapeMarkup = function (markup) { - var replaceMap = { - '\\': '\', - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '/': '/' - }; - - // Do not try to escape the markup if it's not a string - if (typeof markup !== 'string') { - return markup; - } - - return String(markup).replace(/[&<>"'\/\\]/g, function (match) { - return replaceMap[match]; - }); - }; - - // Cache objects in Utils.__cache instead of $.data (see #4346) - Utils.__cache = {}; - - var id = 0; - Utils.GetUniqueElementId = function (element) { - // Get a unique element Id. If element has no id, - // creates a new unique number, stores it in the id - // attribute and returns the new id with a prefix. - // If an id already exists, it simply returns it with a prefix. - - var select2Id = element.getAttribute('data-select2-id'); - - if (select2Id != null) { - return select2Id; - } - - // If element has id, use it. - if (element.id) { - select2Id = 'select2-data-' + element.id; - } else { - select2Id = 'select2-data-' + (++id).toString() + - '-' + Utils.generateChars(4); - } - - element.setAttribute('data-select2-id', select2Id); - - return select2Id; - }; - - Utils.StoreData = function (element, name, value) { - // Stores an item in the cache for a specified element. - // name is the cache key. - var id = Utils.GetUniqueElementId(element); - if (!Utils.__cache[id]) { - Utils.__cache[id] = {}; - } - - Utils.__cache[id][name] = value; - }; - - Utils.GetData = function (element, name) { - // Retrieves a value from the cache by its key (name) - // name is optional. If no name specified, return - // all cache items for the specified element. - // and for a specified element. - var id = Utils.GetUniqueElementId(element); - if (name) { - if (Utils.__cache[id]) { - if (Utils.__cache[id][name] != null) { - return Utils.__cache[id][name]; - } - return $(element).data(name); // Fallback to HTML5 data attribs. - } - return $(element).data(name); // Fallback to HTML5 data attribs. - } else { - return Utils.__cache[id]; - } - }; - - Utils.RemoveData = function (element) { - // Removes all cached items for a specified element. - var id = Utils.GetUniqueElementId(element); - if (Utils.__cache[id] != null) { - delete Utils.__cache[id]; - } - - element.removeAttribute('data-select2-id'); - }; - - Utils.copyNonInternalCssClasses = function (dest, src) { - var classes; - - var destinationClasses = dest.getAttribute('class').trim().split(/\s+/); - - destinationClasses = destinationClasses.filter(function (clazz) { - // Save all Select2 classes - return clazz.indexOf('select2-') === 0; - }); - - var sourceClasses = src.getAttribute('class').trim().split(/\s+/); - - sourceClasses = sourceClasses.filter(function (clazz) { - // Only copy non-Select2 classes - return clazz.indexOf('select2-') !== 0; - }); - - var replacements = destinationClasses.concat(sourceClasses); - - dest.setAttribute('class', replacements.join(' ')); - }; - - return Utils; - }); - - S2.define('select2/results',[ - 'jquery', - './utils' - ], function ($, Utils) { - function Results ($element, options, dataAdapter) { - this.$element = $element; - this.data = dataAdapter; - this.options = options; - - Results.__super__.constructor.call(this); - } - - Utils.Extend(Results, Utils.Observable); - - Results.prototype.render = function () { - var $results = $( - '
    ' - ); - - if (this.options.get('multiple')) { - $results.attr('aria-multiselectable', 'true'); - } - - this.$results = $results; - - return $results; - }; - - Results.prototype.clear = function () { - this.$results.empty(); - }; - - Results.prototype.displayMessage = function (params) { - var escapeMarkup = this.options.get('escapeMarkup'); - - this.clear(); - this.hideLoading(); - - var $message = $( - '' - ); - - var message = this.options.get('translations').get(params.message); - - $message.append( - escapeMarkup( - message(params.args) - ) - ); - - $message[0].className += ' select2-results__message'; - - this.$results.append($message); - }; - - Results.prototype.hideMessages = function () { - this.$results.find('.select2-results__message').remove(); - }; - - Results.prototype.append = function (data) { - this.hideLoading(); - - var $options = []; - - if (data.results == null || data.results.length === 0) { - if (this.$results.children().length === 0) { - this.trigger('results:message', { - message: 'noResults' - }); - } - - return; - } - - data.results = this.sort(data.results); - - for (var d = 0; d < data.results.length; d++) { - var item = data.results[d]; - - var $option = this.option(item); - - $options.push($option); - } - - this.$results.append($options); - }; - - Results.prototype.position = function ($results, $dropdown) { - var $resultsContainer = $dropdown.find('.select2-results'); - $resultsContainer.append($results); - }; - - Results.prototype.sort = function (data) { - var sorter = this.options.get('sorter'); - - return sorter(data); - }; - - Results.prototype.highlightFirstItem = function () { - var $options = this.$results - .find('.select2-results__option--selectable'); - - var $selected = $options.filter('.select2-results__option--selected'); - - // Check if there are any selected options - if ($selected.length > 0) { - // If there are selected options, highlight the first - $selected.first().trigger('mouseenter'); - } else { - // If there are no selected options, highlight the first option - // in the dropdown - $options.first().trigger('mouseenter'); - } - - this.ensureHighlightVisible(); - }; - - Results.prototype.setClasses = function () { - var self = this; - - this.data.current(function (selected) { - var selectedIds = selected.map(function (s) { - return s.id.toString(); - }); - - var $options = self.$results - .find('.select2-results__option--selectable'); - - $options.each(function () { - var $option = $(this); - - var item = Utils.GetData(this, 'data'); - - // id needs to be converted to a string when comparing - var id = '' + item.id; - - if ((item.element != null && item.element.selected) || - (item.element == null && selectedIds.indexOf(id) > -1)) { - this.classList.add('select2-results__option--selected'); - $option.attr('aria-selected', 'true'); - } else { - this.classList.remove('select2-results__option--selected'); - $option.attr('aria-selected', 'false'); - } - }); - - }); - }; - - Results.prototype.showLoading = function (params) { - this.hideLoading(); - - var loadingMore = this.options.get('translations').get('searching'); - - var loading = { - disabled: true, - loading: true, - text: loadingMore(params) - }; - var $loading = this.option(loading); - $loading.className += ' loading-results'; - - this.$results.prepend($loading); - }; - - Results.prototype.hideLoading = function () { - this.$results.find('.loading-results').remove(); - }; - - Results.prototype.option = function (data) { - var option = document.createElement('li'); - option.classList.add('select2-results__option'); - option.classList.add('select2-results__option--selectable'); - - var attrs = { - 'role': 'option' - }; - - var matches = window.Element.prototype.matches || - window.Element.prototype.msMatchesSelector || - window.Element.prototype.webkitMatchesSelector; - - if ((data.element != null && matches.call(data.element, ':disabled')) || - (data.element == null && data.disabled)) { - attrs['aria-disabled'] = 'true'; - - option.classList.remove('select2-results__option--selectable'); - option.classList.add('select2-results__option--disabled'); - } - - if (data.id == null) { - option.classList.remove('select2-results__option--selectable'); - } - - if (data._resultId != null) { - option.id = data._resultId; - } - - if (data.title) { - option.title = data.title; - } - - if (data.children) { - attrs.role = 'group'; - attrs['aria-label'] = data.text; - - option.classList.remove('select2-results__option--selectable'); - option.classList.add('select2-results__option--group'); - } - - for (var attr in attrs) { - var val = attrs[attr]; - - option.setAttribute(attr, val); - } - - if (data.children) { - var $option = $(option); - - var label = document.createElement('strong'); - label.className = 'select2-results__group'; - - this.template(data, label); - - var $children = []; - - for (var c = 0; c < data.children.length; c++) { - var child = data.children[c]; - - var $child = this.option(child); - - $children.push($child); - } - - var $childrenContainer = $('
      ', { - 'class': 'select2-results__options select2-results__options--nested' - }); - - $childrenContainer.append($children); - - $option.append(label); - $option.append($childrenContainer); - } else { - this.template(data, option); - } - - Utils.StoreData(option, 'data', data); - - return option; - }; - - Results.prototype.bind = function (container, $container) { - var self = this; - - var id = container.id + '-results'; - - this.$results.attr('id', id); - - container.on('results:all', function (params) { - self.clear(); - self.append(params.data); - - if (container.isOpen()) { - self.setClasses(); - self.highlightFirstItem(); - } - }); - - container.on('results:append', function (params) { - self.append(params.data); - - if (container.isOpen()) { - self.setClasses(); - } - }); - - container.on('query', function (params) { - self.hideMessages(); - self.showLoading(params); - }); - - container.on('select', function () { - if (!container.isOpen()) { - return; - } - - self.setClasses(); - - if (self.options.get('scrollAfterSelect')) { - self.highlightFirstItem(); - } - }); - - container.on('unselect', function () { - if (!container.isOpen()) { - return; - } - - self.setClasses(); - - if (self.options.get('scrollAfterSelect')) { - self.highlightFirstItem(); - } - }); - - container.on('open', function () { - // When the dropdown is open, aria-expended="true" - self.$results.attr('aria-expanded', 'true'); - self.$results.attr('aria-hidden', 'false'); - - self.setClasses(); - self.ensureHighlightVisible(); - }); - - container.on('close', function () { - // When the dropdown is closed, aria-expended="false" - self.$results.attr('aria-expanded', 'false'); - self.$results.attr('aria-hidden', 'true'); - self.$results.removeAttr('aria-activedescendant'); - }); - - container.on('results:toggle', function () { - var $highlighted = self.getHighlightedResults(); - - if ($highlighted.length === 0) { - return; - } - - $highlighted.trigger('mouseup'); - }); - - container.on('results:select', function () { - var $highlighted = self.getHighlightedResults(); - - if ($highlighted.length === 0) { - return; - } - - var data = Utils.GetData($highlighted[0], 'data'); - - if ($highlighted.hasClass('select2-results__option--selected')) { - self.trigger('close', {}); - } else { - self.trigger('select', { - data: data - }); - } - }); - - container.on('results:previous', function () { - var $highlighted = self.getHighlightedResults(); - - var $options = self.$results.find('.select2-results__option--selectable'); - - var currentIndex = $options.index($highlighted); - - // If we are already at the top, don't move further - // If no options, currentIndex will be -1 - if (currentIndex <= 0) { - return; - } - - var nextIndex = currentIndex - 1; - - // If none are highlighted, highlight the first - if ($highlighted.length === 0) { - nextIndex = 0; - } - - var $next = $options.eq(nextIndex); - - $next.trigger('mouseenter'); - - var currentOffset = self.$results.offset().top; - var nextTop = $next.offset().top; - var nextOffset = self.$results.scrollTop() + (nextTop - currentOffset); - - if (nextIndex === 0) { - self.$results.scrollTop(0); - } else if (nextTop - currentOffset < 0) { - self.$results.scrollTop(nextOffset); - } - }); - - container.on('results:next', function () { - var $highlighted = self.getHighlightedResults(); - - var $options = self.$results.find('.select2-results__option--selectable'); - - var currentIndex = $options.index($highlighted); - - var nextIndex = currentIndex + 1; - - // If we are at the last option, stay there - if (nextIndex >= $options.length) { - return; - } - - var $next = $options.eq(nextIndex); - - $next.trigger('mouseenter'); - - var currentOffset = self.$results.offset().top + - self.$results.outerHeight(false); - var nextBottom = $next.offset().top + $next.outerHeight(false); - var nextOffset = self.$results.scrollTop() + nextBottom - currentOffset; - - if (nextIndex === 0) { - self.$results.scrollTop(0); - } else if (nextBottom > currentOffset) { - self.$results.scrollTop(nextOffset); - } - }); - - container.on('results:focus', function (params) { - params.element[0].classList.add('select2-results__option--highlighted'); - params.element[0].setAttribute('aria-selected', 'true'); - }); - - container.on('results:message', function (params) { - self.displayMessage(params); - }); - - if ($.fn.mousewheel) { - this.$results.on('mousewheel', function (e) { - var top = self.$results.scrollTop(); - - var bottom = self.$results.get(0).scrollHeight - top + e.deltaY; - - var isAtTop = e.deltaY > 0 && top - e.deltaY <= 0; - var isAtBottom = e.deltaY < 0 && bottom <= self.$results.height(); - - if (isAtTop) { - self.$results.scrollTop(0); - - e.preventDefault(); - e.stopPropagation(); - } else if (isAtBottom) { - self.$results.scrollTop( - self.$results.get(0).scrollHeight - self.$results.height() - ); - - e.preventDefault(); - e.stopPropagation(); - } - }); - } - - this.$results.on('mouseup', '.select2-results__option--selectable', - function (evt) { - var $this = $(this); - - var data = Utils.GetData(this, 'data'); - - if ($this.hasClass('select2-results__option--selected')) { - if (self.options.get('multiple')) { - self.trigger('unselect', { - originalEvent: evt, - data: data - }); - } else { - self.trigger('close', {}); - } - - return; - } - - self.trigger('select', { - originalEvent: evt, - data: data - }); - }); - - this.$results.on('mouseenter', '.select2-results__option--selectable', - function (evt) { - var data = Utils.GetData(this, 'data'); - - self.getHighlightedResults() - .removeClass('select2-results__option--highlighted') - .attr('aria-selected', 'false'); - - self.trigger('results:focus', { - data: data, - element: $(this) - }); - }); - }; - - Results.prototype.getHighlightedResults = function () { - var $highlighted = this.$results - .find('.select2-results__option--highlighted'); - - return $highlighted; - }; - - Results.prototype.destroy = function () { - this.$results.remove(); - }; - - Results.prototype.ensureHighlightVisible = function () { - var $highlighted = this.getHighlightedResults(); - - if ($highlighted.length === 0) { - return; - } - - var $options = this.$results.find('.select2-results__option--selectable'); - - var currentIndex = $options.index($highlighted); - - var currentOffset = this.$results.offset().top; - var nextTop = $highlighted.offset().top; - var nextOffset = this.$results.scrollTop() + (nextTop - currentOffset); - - var offsetDelta = nextTop - currentOffset; - nextOffset -= $highlighted.outerHeight(false) * 2; - - if (currentIndex <= 2) { - this.$results.scrollTop(0); - } else if (offsetDelta > this.$results.outerHeight() || offsetDelta < 0) { - this.$results.scrollTop(nextOffset); - } - }; - - Results.prototype.template = function (result, container) { - var template = this.options.get('templateResult'); - var escapeMarkup = this.options.get('escapeMarkup'); - - var content = template(result, container); - - if (content == null) { - container.style.display = 'none'; - } else if (typeof content === 'string') { - container.innerHTML = escapeMarkup(content); - } else { - $(container).append(content); - } - }; - - return Results; - }); - - S2.define('select2/keys',[ - - ], function () { - var KEYS = { - BACKSPACE: 8, - TAB: 9, - ENTER: 13, - SHIFT: 16, - CTRL: 17, - ALT: 18, - ESC: 27, - SPACE: 32, - PAGE_UP: 33, - PAGE_DOWN: 34, - END: 35, - HOME: 36, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - DELETE: 46 - }; - - return KEYS; - }); - - S2.define('select2/selection/base',[ - 'jquery', - '../utils', - '../keys' - ], function ($, Utils, KEYS) { - function BaseSelection ($element, options) { - this.$element = $element; - this.options = options; - - BaseSelection.__super__.constructor.call(this); - } - - Utils.Extend(BaseSelection, Utils.Observable); - - BaseSelection.prototype.render = function () { - var $selection = $( - '' - ); - - this._tabindex = 0; - - if (Utils.GetData(this.$element[0], 'old-tabindex') != null) { - this._tabindex = Utils.GetData(this.$element[0], 'old-tabindex'); - } else if (this.$element.attr('tabindex') != null) { - this._tabindex = this.$element.attr('tabindex'); - } - - $selection.attr('title', this.$element.attr('title')); - $selection.attr('tabindex', this._tabindex); - $selection.attr('aria-disabled', 'false'); - - this.$selection = $selection; - - return $selection; - }; - - BaseSelection.prototype.bind = function (container, $container) { - var self = this; - - var resultsId = container.id + '-results'; - - this.container = container; - - this.$selection.on('focus', function (evt) { - self.trigger('focus', evt); - }); - - this.$selection.on('blur', function (evt) { - self._handleBlur(evt); - }); - - this.$selection.on('keydown', function (evt) { - self.trigger('keypress', evt); - - if (evt.which === KEYS.SPACE) { - evt.preventDefault(); - } - }); - - container.on('results:focus', function (params) { - self.$selection.attr('aria-activedescendant', params.data._resultId); - }); - - container.on('selection:update', function (params) { - self.update(params.data); - }); - - container.on('open', function () { - // When the dropdown is open, aria-expanded="true" - self.$selection.attr('aria-expanded', 'true'); - self.$selection.attr('aria-owns', resultsId); - - self._attachCloseHandler(container); - }); - - container.on('close', function () { - // When the dropdown is closed, aria-expanded="false" - self.$selection.attr('aria-expanded', 'false'); - self.$selection.removeAttr('aria-activedescendant'); - self.$selection.removeAttr('aria-owns'); - - self.$selection.trigger('focus'); - - self._detachCloseHandler(container); - }); - - container.on('enable', function () { - self.$selection.attr('tabindex', self._tabindex); - self.$selection.attr('aria-disabled', 'false'); - }); - - container.on('disable', function () { - self.$selection.attr('tabindex', '-1'); - self.$selection.attr('aria-disabled', 'true'); - }); - }; - - BaseSelection.prototype._handleBlur = function (evt) { - var self = this; - - // This needs to be delayed as the active element is the body when the tab - // key is pressed, possibly along with others. - window.setTimeout(function () { - // Don't trigger `blur` if the focus is still in the selection - if ( - (document.activeElement == self.$selection[0]) || - ($.contains(self.$selection[0], document.activeElement)) - ) { - return; - } - - self.trigger('blur', evt); - }, 1); - }; - - BaseSelection.prototype._attachCloseHandler = function (container) { - - $(document.body).on('mousedown.select2.' + container.id, function (e) { - var $target = $(e.target); - - var $select = $target.closest('.select2'); - - var $all = $('.select2.select2-container--open'); - - $all.each(function () { - if (this == $select[0]) { - return; - } - - var $element = Utils.GetData(this, 'element'); - - $element.select2('close'); - }); - }); - }; - - BaseSelection.prototype._detachCloseHandler = function (container) { - $(document.body).off('mousedown.select2.' + container.id); - }; - - BaseSelection.prototype.position = function ($selection, $container) { - var $selectionContainer = $container.find('.selection'); - $selectionContainer.append($selection); - }; - - BaseSelection.prototype.destroy = function () { - this._detachCloseHandler(this.container); - }; - - BaseSelection.prototype.update = function (data) { - throw new Error('The `update` method must be defined in child classes.'); - }; - - /** - * Helper method to abstract the "enabled" (not "disabled") state of this - * object. - * - * @return {true} if the instance is not disabled. - * @return {false} if the instance is disabled. - */ - BaseSelection.prototype.isEnabled = function () { - return !this.isDisabled(); - }; - - /** - * Helper method to abstract the "disabled" state of this object. - * - * @return {true} if the disabled option is true. - * @return {false} if the disabled option is false. - */ - BaseSelection.prototype.isDisabled = function () { - return this.options.get('disabled'); - }; - - return BaseSelection; - }); - - S2.define('select2/selection/single',[ - 'jquery', - './base', - '../utils', - '../keys' - ], function ($, BaseSelection, Utils, KEYS) { - function SingleSelection () { - SingleSelection.__super__.constructor.apply(this, arguments); - } - - Utils.Extend(SingleSelection, BaseSelection); - - SingleSelection.prototype.render = function () { - var $selection = SingleSelection.__super__.render.call(this); - - $selection.addClass('select2-selection--single'); - - $selection.html( - '' + - '' + - '' + - '' - ); - - return $selection; - }; - - SingleSelection.prototype.bind = function (container, $container) { - var self = this; - - SingleSelection.__super__.bind.apply(this, arguments); - - var id = container.id + '-container'; - var first_click_datetime = new Date(); - - this.$selection.find('.select2-selection__rendered') - .attr('id', id) - .attr('role', 'textbox') - .attr('aria-readonly', 'true'); - this.$selection.attr('aria-labelledby', id); - - this.$selection.on('mousedown', function (evt) { - var second_click_datetime = new Date(); - //We add a time guard to make sure this button can only be clicked once in 200ms - if (second_click_datetime > first_click_datetime){ - // Only respond to left clicks - if (evt.which !== 1) { - return; - } - self.trigger('toggle', { - originalEvent: evt - }); - //set the first click time to 200 ms later. - first_click_datetime=second_click_datetime.setMilliseconds(second_click_datetime.getMilliseconds() + 200); - } - }); - - this.$selection.on('focus', function (evt) { - // User focuses on the container - }); - - this.$selection.on('blur', function (evt) { - // User exits the container - }); - - container.on('focus', function (evt) { - if (!container.isOpen()) { - self.$selection.trigger('focus'); - } - }); - }; - - SingleSelection.prototype.clear = function () { - var $rendered = this.$selection.find('.select2-selection__rendered'); - $rendered.empty(); - $rendered.removeAttr('title'); // clear tooltip on empty - }; - - SingleSelection.prototype.display = function (data, container) { - var template = this.options.get('templateSelection'); - var escapeMarkup = this.options.get('escapeMarkup'); - - return escapeMarkup(template(data, container)); - }; - - SingleSelection.prototype.selectionContainer = function () { - return $(''); - }; - - SingleSelection.prototype.update = function (data) { - if (data.length === 0) { - this.clear(); - return; - } - - var selection = data[0]; - - var $rendered = this.$selection.find('.select2-selection__rendered'); - var formatted = this.display(selection, $rendered); - - $rendered.empty().append(formatted); - - var title = selection.title || selection.text; - - if (title) { - $rendered.attr('title', title); - } else { - $rendered.removeAttr('title'); - } - }; - - return SingleSelection; - }); - - S2.define('select2/selection/multiple',[ - 'jquery', - './base', - '../utils' - ], function ($, BaseSelection, Utils) { - function MultipleSelection ($element, options) { - MultipleSelection.__super__.constructor.apply(this, arguments); - } - - Utils.Extend(MultipleSelection, BaseSelection); - - MultipleSelection.prototype.render = function () { - var $selection = MultipleSelection.__super__.render.call(this); - - $selection[0].classList.add('select2-selection--multiple'); - - $selection.html( - '
        ' - ); - - return $selection; - }; - - MultipleSelection.prototype.bind = function (container, $container) { - var self = this; - - MultipleSelection.__super__.bind.apply(this, arguments); - - var id = container.id + '-container'; - this.$selection.find('.select2-selection__rendered').attr('id', id); - - this.$selection.on('click', function (evt) { - self.trigger('toggle', { - originalEvent: evt - }); - }); - - this.$selection.on( - 'click', - '.select2-selection__choice__remove', - function (evt) { - // Ignore the event if it is disabled - if (self.isDisabled()) { - return; - } - - var $remove = $(this); - var $selection = $remove.parent(); - - var data = Utils.GetData($selection[0], 'data'); - - self.trigger('unselect', { - originalEvent: evt, - data: data - }); - } - ); - - this.$selection.on( - 'keydown', - '.select2-selection__choice__remove', - function (evt) { - // Ignore the event if it is disabled - if (self.isDisabled()) { - return; - } - - evt.stopPropagation(); - } - ); - }; - - MultipleSelection.prototype.clear = function () { - var $rendered = this.$selection.find('.select2-selection__rendered'); - $rendered.empty(); - $rendered.removeAttr('title'); - }; - - MultipleSelection.prototype.display = function (data, container) { - var template = this.options.get('templateSelection'); - var escapeMarkup = this.options.get('escapeMarkup'); - - return escapeMarkup(template(data, container)); - }; - - MultipleSelection.prototype.selectionContainer = function () { - var $container = $( - '
      • ' + - '' + - '' + - '
      • ' - ); - - return $container; - }; - - MultipleSelection.prototype.update = function (data) { - this.clear(); - - if (data.length === 0) { - return; - } - - var $selections = []; - - var selectionIdPrefix = this.$selection.find('.select2-selection__rendered') - .attr('id') + '-choice-'; - - for (var d = 0; d < data.length; d++) { - var selection = data[d]; - - var $selection = this.selectionContainer(); - var formatted = this.display(selection, $selection); - - var selectionId = selectionIdPrefix + Utils.generateChars(4) + '-'; - - if (selection.id) { - selectionId += selection.id; - } else { - selectionId += Utils.generateChars(4); - } - - $selection.find('.select2-selection__choice__display') - .append(formatted) - .attr('id', selectionId); - - var title = selection.title || selection.text; - - if (title) { - $selection.attr('title', title); - } - - var removeItem = this.options.get('translations').get('removeItem'); - - var $remove = $selection.find('.select2-selection__choice__remove'); - - $remove.attr('title', removeItem()); - $remove.attr('aria-label', removeItem()); - $remove.attr('aria-describedby', selectionId); - - Utils.StoreData($selection[0], 'data', selection); - - $selections.push($selection); - } - - var $rendered = this.$selection.find('.select2-selection__rendered'); - - $rendered.append($selections); - }; - - return MultipleSelection; - }); - - S2.define('select2/selection/placeholder',[ - - ], function () { - function Placeholder (decorated, $element, options) { - this.placeholder = this.normalizePlaceholder(options.get('placeholder')); - - decorated.call(this, $element, options); - } - - Placeholder.prototype.normalizePlaceholder = function (_, placeholder) { - if (typeof placeholder === 'string') { - placeholder = { - id: '', - text: placeholder - }; - } - - return placeholder; - }; - - Placeholder.prototype.createPlaceholder = function (decorated, placeholder) { - var $placeholder = this.selectionContainer(); - - $placeholder.html(this.display(placeholder)); - $placeholder[0].classList.add('select2-selection__placeholder'); - $placeholder[0].classList.remove('select2-selection__choice'); - - return $placeholder; - }; - - Placeholder.prototype.update = function (decorated, data) { - var singlePlaceholder = ( - data.length == 1 && data[0].id != this.placeholder.id - ); - var multipleSelections = data.length > 1; - - if (multipleSelections || singlePlaceholder) { - return decorated.call(this, data); - } - - this.clear(); - - var $placeholder = this.createPlaceholder(this.placeholder); - - this.$selection.find('.select2-selection__rendered').append($placeholder); - }; - - return Placeholder; - }); - - S2.define('select2/selection/allowClear',[ - 'jquery', - '../keys', - '../utils' - ], function ($, KEYS, Utils) { - function AllowClear () { } - - AllowClear.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - if (this.placeholder == null) { - if (this.options.get('debug') && window.console && console.error) { - console.error( - 'Select2: The `allowClear` option should be used in combination ' + - 'with the `placeholder` option.' - ); - } - } - - this.$selection.on('mousedown', '.select2-selection__clear', - function (evt) { - self._handleClear(evt); - }); - - container.on('keypress', function (evt) { - self._handleKeyboardClear(evt, container); - }); - }; - - AllowClear.prototype._handleClear = function (_, evt) { - // Ignore the event if it is disabled - if (this.isDisabled()) { - return; - } - - var $clear = this.$selection.find('.select2-selection__clear'); - - // Ignore the event if nothing has been selected - if ($clear.length === 0) { - return; - } - - evt.stopPropagation(); - - var data = Utils.GetData($clear[0], 'data'); - - var previousVal = this.$element.val(); - this.$element.val(this.placeholder.id); - - var unselectData = { - data: data - }; - this.trigger('clear', unselectData); - if (unselectData.prevented) { - this.$element.val(previousVal); - return; - } - - for (var d = 0; d < data.length; d++) { - unselectData = { - data: data[d] - }; - - // Trigger the `unselect` event, so people can prevent it from being - // cleared. - this.trigger('unselect', unselectData); - - // If the event was prevented, don't clear it out. - if (unselectData.prevented) { - this.$element.val(previousVal); - return; - } - } - - this.$element.trigger('input').trigger('change'); - - this.trigger('toggle', {}); - }; - - AllowClear.prototype._handleKeyboardClear = function (_, evt, container) { - if (container.isOpen()) { - return; - } - - if (evt.which == KEYS.DELETE || evt.which == KEYS.BACKSPACE) { - this._handleClear(evt); - } - }; - - AllowClear.prototype.update = function (decorated, data) { - decorated.call(this, data); - - this.$selection.find('.select2-selection__clear').remove(); - - if (this.$selection.find('.select2-selection__placeholder').length > 0 || - data.length === 0) { - return; - } - - var selectionId = this.$selection.find('.select2-selection__rendered') - .attr('id'); - - var removeAll = this.options.get('translations').get('removeAllItems'); - - var $remove = $( - '' - ); - $remove.attr('title', removeAll()); - $remove.attr('aria-label', removeAll()); - $remove.attr('aria-describedby', selectionId); - Utils.StoreData($remove[0], 'data', data); - - this.$selection.prepend($remove); - }; - - return AllowClear; - }); - - S2.define('select2/selection/search',[ - 'jquery', - '../utils', - '../keys' - ], function ($, Utils, KEYS) { - function Search (decorated, $element, options) { - decorated.call(this, $element, options); - } - - Search.prototype.render = function (decorated) { - var $search = $( - '' + - '' + - '' - ); - - this.$searchContainer = $search; - this.$search = $search.find('input'); - - this.$search.prop('autocomplete', this.options.get('autocomplete')); - - var $rendered = decorated.call(this); - - this._transferTabIndex(); - $rendered.append(this.$searchContainer); - - return $rendered; - }; - - Search.prototype.bind = function (decorated, container, $container) { - var self = this; - - var resultsId = container.id + '-results'; - var selectionId = container.id + '-container'; - - decorated.call(this, container, $container); - - self.$search.attr('aria-describedby', selectionId); - - container.on('open', function () { - self.$search.attr('aria-controls', resultsId); - self.$search.trigger('focus'); - }); - - container.on('close', function () { - self.$search.val(''); - self.resizeSearch(); - self.$search.removeAttr('aria-controls'); - self.$search.removeAttr('aria-activedescendant'); - self.$search.trigger('focus'); - }); - - container.on('enable', function () { - self.$search.prop('disabled', false); - - self._transferTabIndex(); - }); - - container.on('disable', function () { - self.$search.prop('disabled', true); - }); - - container.on('focus', function (evt) { - self.$search.trigger('focus'); - }); - - container.on('results:focus', function (params) { - if (params.data._resultId) { - self.$search.attr('aria-activedescendant', params.data._resultId); - } else { - self.$search.removeAttr('aria-activedescendant'); - } - }); - - this.$selection.on('focusin', '.select2-search--inline', function (evt) { - self.trigger('focus', evt); - }); - - this.$selection.on('focusout', '.select2-search--inline', function (evt) { - self._handleBlur(evt); - }); - - this.$selection.on('keydown', '.select2-search--inline', function (evt) { - evt.stopPropagation(); - - self.trigger('keypress', evt); - - self._keyUpPrevented = evt.isDefaultPrevented(); - - var key = evt.which; - - if (key === KEYS.BACKSPACE && self.$search.val() === '') { - var $previousChoice = self.$selection - .find('.select2-selection__choice').last(); - - if ($previousChoice.length > 0) { - var item = Utils.GetData($previousChoice[0], 'data'); - - self.searchRemoveChoice(item); - - evt.preventDefault(); - } - } - }); - - this.$selection.on('click', '.select2-search--inline', function (evt) { - if (self.$search.val()) { - evt.stopPropagation(); - } - }); - - // Try to detect the IE version should the `documentMode` property that - // is stored on the document. This is only implemented in IE and is - // slightly cleaner than doing a user agent check. - // This property is not available in Edge, but Edge also doesn't have - // this bug. - var msie = document.documentMode; - var disableInputEvents = msie && msie <= 11; - - // Workaround for browsers which do not support the `input` event - // This will prevent double-triggering of events for browsers which support - // both the `keyup` and `input` events. - this.$selection.on( - 'input.searchcheck', - '.select2-search--inline', - function (evt) { - // IE will trigger the `input` event when a placeholder is used on a - // search box. To get around this issue, we are forced to ignore all - // `input` events in IE and keep using `keyup`. - if (disableInputEvents) { - self.$selection.off('input.search input.searchcheck'); - return; - } - - // Unbind the duplicated `keyup` event - self.$selection.off('keyup.search'); - } - ); - - this.$selection.on( - 'keyup.search input.search', - '.select2-search--inline', - function (evt) { - // IE will trigger the `input` event when a placeholder is used on a - // search box. To get around this issue, we are forced to ignore all - // `input` events in IE and keep using `keyup`. - if (disableInputEvents && evt.type === 'input') { - self.$selection.off('input.search input.searchcheck'); - return; - } - - var key = evt.which; - - // We can freely ignore events from modifier keys - if (key == KEYS.SHIFT || key == KEYS.CTRL || key == KEYS.ALT) { - return; - } - - // Tabbing will be handled during the `keydown` phase - if (key == KEYS.TAB) { - return; - } - - self.handleSearch(evt); - } - ); - }; - - /** - * This method will transfer the tabindex attribute from the rendered - * selection to the search box. This allows for the search box to be used as - * the primary focus instead of the selection container. - * - * @private - */ - Search.prototype._transferTabIndex = function (decorated) { - this.$search.attr('tabindex', this.$selection.attr('tabindex')); - this.$selection.attr('tabindex', '-1'); - }; - - Search.prototype.createPlaceholder = function (decorated, placeholder) { - this.$search.attr('placeholder', placeholder.text); - }; - - Search.prototype.update = function (decorated, data) { - var searchHadFocus = this.$search[0] == document.activeElement; - - this.$search.attr('placeholder', ''); - - decorated.call(this, data); - - this.resizeSearch(); - if (searchHadFocus) { - this.$search.trigger('focus'); - } - }; - - Search.prototype.handleSearch = function () { - this.resizeSearch(); - - if (!this._keyUpPrevented) { - var input = this.$search.val(); - - this.trigger('query', { - term: input - }); - } - - this._keyUpPrevented = false; - }; - - Search.prototype.searchRemoveChoice = function (decorated, item) { - this.trigger('unselect', { - data: item - }); - - this.$search.val(item.text); - this.handleSearch(); - }; - - Search.prototype.resizeSearch = function () { - this.$search.css('width', '25px'); - - var width = '100%'; - - if (this.$search.attr('placeholder') === '') { - var minimumWidth = this.$search.val().length + 1; - - width = (minimumWidth * 0.75) + 'em'; - } - - this.$search.css('width', width); - }; - - return Search; - }); - - S2.define('select2/selection/selectionCss',[ - '../utils' - ], function (Utils) { - function SelectionCSS () { } - - SelectionCSS.prototype.render = function (decorated) { - var $selection = decorated.call(this); - - var selectionCssClass = this.options.get('selectionCssClass') || ''; - - if (selectionCssClass.indexOf(':all:') !== -1) { - selectionCssClass = selectionCssClass.replace(':all:', ''); - - Utils.copyNonInternalCssClasses($selection[0], this.$element[0]); - } - - $selection.addClass(selectionCssClass); - - return $selection; - }; - - return SelectionCSS; - }); - - S2.define('select2/selection/eventRelay',[ - 'jquery' - ], function ($) { - function EventRelay () { } - - EventRelay.prototype.bind = function (decorated, container, $container) { - var self = this; - var relayEvents = [ - 'open', 'opening', - 'close', 'closing', - 'select', 'selecting', - 'unselect', 'unselecting', - 'clear', 'clearing' - ]; - - var preventableEvents = [ - 'opening', 'closing', 'selecting', 'unselecting', 'clearing' - ]; - - decorated.call(this, container, $container); - - container.on('*', function (name, params) { - // Ignore events that should not be relayed - if (relayEvents.indexOf(name) === -1) { - return; - } - - // The parameters should always be an object - params = params || {}; - - // Generate the jQuery event for the Select2 event - var evt = $.Event('select2:' + name, { - params: params - }); - - self.$element.trigger(evt); - - // Only handle preventable events if it was one - if (preventableEvents.indexOf(name) === -1) { - return; - } - - params.prevented = evt.isDefaultPrevented(); - }); - }; - - return EventRelay; - }); - - S2.define('select2/translation',[ - 'jquery', - 'require' - ], function ($, require) { - function Translation (dict) { - this.dict = dict || {}; - } - - Translation.prototype.all = function () { - return this.dict; - }; - - Translation.prototype.get = function (key) { - return this.dict[key]; - }; - - Translation.prototype.extend = function (translation) { - this.dict = $.extend({}, translation.all(), this.dict); - }; - - // Static functions - - Translation._cache = {}; - - Translation.loadPath = function (path) { - if (!(path in Translation._cache)) { - var translations = require(path); - - Translation._cache[path] = translations; - } - - return new Translation(Translation._cache[path]); - }; - - return Translation; - }); - - S2.define('select2/diacritics',[ - - ], function () { - var diacritics = { - '\u24B6': 'A', - '\uFF21': 'A', - '\u00C0': 'A', - '\u00C1': 'A', - '\u00C2': 'A', - '\u1EA6': 'A', - '\u1EA4': 'A', - '\u1EAA': 'A', - '\u1EA8': 'A', - '\u00C3': 'A', - '\u0100': 'A', - '\u0102': 'A', - '\u1EB0': 'A', - '\u1EAE': 'A', - '\u1EB4': 'A', - '\u1EB2': 'A', - '\u0226': 'A', - '\u01E0': 'A', - '\u00C4': 'A', - '\u01DE': 'A', - '\u1EA2': 'A', - '\u00C5': 'A', - '\u01FA': 'A', - '\u01CD': 'A', - '\u0200': 'A', - '\u0202': 'A', - '\u1EA0': 'A', - '\u1EAC': 'A', - '\u1EB6': 'A', - '\u1E00': 'A', - '\u0104': 'A', - '\u023A': 'A', - '\u2C6F': 'A', - '\uA732': 'AA', - '\u00C6': 'AE', - '\u01FC': 'AE', - '\u01E2': 'AE', - '\uA734': 'AO', - '\uA736': 'AU', - '\uA738': 'AV', - '\uA73A': 'AV', - '\uA73C': 'AY', - '\u24B7': 'B', - '\uFF22': 'B', - '\u1E02': 'B', - '\u1E04': 'B', - '\u1E06': 'B', - '\u0243': 'B', - '\u0182': 'B', - '\u0181': 'B', - '\u24B8': 'C', - '\uFF23': 'C', - '\u0106': 'C', - '\u0108': 'C', - '\u010A': 'C', - '\u010C': 'C', - '\u00C7': 'C', - '\u1E08': 'C', - '\u0187': 'C', - '\u023B': 'C', - '\uA73E': 'C', - '\u24B9': 'D', - '\uFF24': 'D', - '\u1E0A': 'D', - '\u010E': 'D', - '\u1E0C': 'D', - '\u1E10': 'D', - '\u1E12': 'D', - '\u1E0E': 'D', - '\u0110': 'D', - '\u018B': 'D', - '\u018A': 'D', - '\u0189': 'D', - '\uA779': 'D', - '\u01F1': 'DZ', - '\u01C4': 'DZ', - '\u01F2': 'Dz', - '\u01C5': 'Dz', - '\u24BA': 'E', - '\uFF25': 'E', - '\u00C8': 'E', - '\u00C9': 'E', - '\u00CA': 'E', - '\u1EC0': 'E', - '\u1EBE': 'E', - '\u1EC4': 'E', - '\u1EC2': 'E', - '\u1EBC': 'E', - '\u0112': 'E', - '\u1E14': 'E', - '\u1E16': 'E', - '\u0114': 'E', - '\u0116': 'E', - '\u00CB': 'E', - '\u1EBA': 'E', - '\u011A': 'E', - '\u0204': 'E', - '\u0206': 'E', - '\u1EB8': 'E', - '\u1EC6': 'E', - '\u0228': 'E', - '\u1E1C': 'E', - '\u0118': 'E', - '\u1E18': 'E', - '\u1E1A': 'E', - '\u0190': 'E', - '\u018E': 'E', - '\u24BB': 'F', - '\uFF26': 'F', - '\u1E1E': 'F', - '\u0191': 'F', - '\uA77B': 'F', - '\u24BC': 'G', - '\uFF27': 'G', - '\u01F4': 'G', - '\u011C': 'G', - '\u1E20': 'G', - '\u011E': 'G', - '\u0120': 'G', - '\u01E6': 'G', - '\u0122': 'G', - '\u01E4': 'G', - '\u0193': 'G', - '\uA7A0': 'G', - '\uA77D': 'G', - '\uA77E': 'G', - '\u24BD': 'H', - '\uFF28': 'H', - '\u0124': 'H', - '\u1E22': 'H', - '\u1E26': 'H', - '\u021E': 'H', - '\u1E24': 'H', - '\u1E28': 'H', - '\u1E2A': 'H', - '\u0126': 'H', - '\u2C67': 'H', - '\u2C75': 'H', - '\uA78D': 'H', - '\u24BE': 'I', - '\uFF29': 'I', - '\u00CC': 'I', - '\u00CD': 'I', - '\u00CE': 'I', - '\u0128': 'I', - '\u012A': 'I', - '\u012C': 'I', - '\u0130': 'I', - '\u00CF': 'I', - '\u1E2E': 'I', - '\u1EC8': 'I', - '\u01CF': 'I', - '\u0208': 'I', - '\u020A': 'I', - '\u1ECA': 'I', - '\u012E': 'I', - '\u1E2C': 'I', - '\u0197': 'I', - '\u24BF': 'J', - '\uFF2A': 'J', - '\u0134': 'J', - '\u0248': 'J', - '\u24C0': 'K', - '\uFF2B': 'K', - '\u1E30': 'K', - '\u01E8': 'K', - '\u1E32': 'K', - '\u0136': 'K', - '\u1E34': 'K', - '\u0198': 'K', - '\u2C69': 'K', - '\uA740': 'K', - '\uA742': 'K', - '\uA744': 'K', - '\uA7A2': 'K', - '\u24C1': 'L', - '\uFF2C': 'L', - '\u013F': 'L', - '\u0139': 'L', - '\u013D': 'L', - '\u1E36': 'L', - '\u1E38': 'L', - '\u013B': 'L', - '\u1E3C': 'L', - '\u1E3A': 'L', - '\u0141': 'L', - '\u023D': 'L', - '\u2C62': 'L', - '\u2C60': 'L', - '\uA748': 'L', - '\uA746': 'L', - '\uA780': 'L', - '\u01C7': 'LJ', - '\u01C8': 'Lj', - '\u24C2': 'M', - '\uFF2D': 'M', - '\u1E3E': 'M', - '\u1E40': 'M', - '\u1E42': 'M', - '\u2C6E': 'M', - '\u019C': 'M', - '\u24C3': 'N', - '\uFF2E': 'N', - '\u01F8': 'N', - '\u0143': 'N', - '\u00D1': 'N', - '\u1E44': 'N', - '\u0147': 'N', - '\u1E46': 'N', - '\u0145': 'N', - '\u1E4A': 'N', - '\u1E48': 'N', - '\u0220': 'N', - '\u019D': 'N', - '\uA790': 'N', - '\uA7A4': 'N', - '\u01CA': 'NJ', - '\u01CB': 'Nj', - '\u24C4': 'O', - '\uFF2F': 'O', - '\u00D2': 'O', - '\u00D3': 'O', - '\u00D4': 'O', - '\u1ED2': 'O', - '\u1ED0': 'O', - '\u1ED6': 'O', - '\u1ED4': 'O', - '\u00D5': 'O', - '\u1E4C': 'O', - '\u022C': 'O', - '\u1E4E': 'O', - '\u014C': 'O', - '\u1E50': 'O', - '\u1E52': 'O', - '\u014E': 'O', - '\u022E': 'O', - '\u0230': 'O', - '\u00D6': 'O', - '\u022A': 'O', - '\u1ECE': 'O', - '\u0150': 'O', - '\u01D1': 'O', - '\u020C': 'O', - '\u020E': 'O', - '\u01A0': 'O', - '\u1EDC': 'O', - '\u1EDA': 'O', - '\u1EE0': 'O', - '\u1EDE': 'O', - '\u1EE2': 'O', - '\u1ECC': 'O', - '\u1ED8': 'O', - '\u01EA': 'O', - '\u01EC': 'O', - '\u00D8': 'O', - '\u01FE': 'O', - '\u0186': 'O', - '\u019F': 'O', - '\uA74A': 'O', - '\uA74C': 'O', - '\u0152': 'OE', - '\u01A2': 'OI', - '\uA74E': 'OO', - '\u0222': 'OU', - '\u24C5': 'P', - '\uFF30': 'P', - '\u1E54': 'P', - '\u1E56': 'P', - '\u01A4': 'P', - '\u2C63': 'P', - '\uA750': 'P', - '\uA752': 'P', - '\uA754': 'P', - '\u24C6': 'Q', - '\uFF31': 'Q', - '\uA756': 'Q', - '\uA758': 'Q', - '\u024A': 'Q', - '\u24C7': 'R', - '\uFF32': 'R', - '\u0154': 'R', - '\u1E58': 'R', - '\u0158': 'R', - '\u0210': 'R', - '\u0212': 'R', - '\u1E5A': 'R', - '\u1E5C': 'R', - '\u0156': 'R', - '\u1E5E': 'R', - '\u024C': 'R', - '\u2C64': 'R', - '\uA75A': 'R', - '\uA7A6': 'R', - '\uA782': 'R', - '\u24C8': 'S', - '\uFF33': 'S', - '\u1E9E': 'S', - '\u015A': 'S', - '\u1E64': 'S', - '\u015C': 'S', - '\u1E60': 'S', - '\u0160': 'S', - '\u1E66': 'S', - '\u1E62': 'S', - '\u1E68': 'S', - '\u0218': 'S', - '\u015E': 'S', - '\u2C7E': 'S', - '\uA7A8': 'S', - '\uA784': 'S', - '\u24C9': 'T', - '\uFF34': 'T', - '\u1E6A': 'T', - '\u0164': 'T', - '\u1E6C': 'T', - '\u021A': 'T', - '\u0162': 'T', - '\u1E70': 'T', - '\u1E6E': 'T', - '\u0166': 'T', - '\u01AC': 'T', - '\u01AE': 'T', - '\u023E': 'T', - '\uA786': 'T', - '\uA728': 'TZ', - '\u24CA': 'U', - '\uFF35': 'U', - '\u00D9': 'U', - '\u00DA': 'U', - '\u00DB': 'U', - '\u0168': 'U', - '\u1E78': 'U', - '\u016A': 'U', - '\u1E7A': 'U', - '\u016C': 'U', - '\u00DC': 'U', - '\u01DB': 'U', - '\u01D7': 'U', - '\u01D5': 'U', - '\u01D9': 'U', - '\u1EE6': 'U', - '\u016E': 'U', - '\u0170': 'U', - '\u01D3': 'U', - '\u0214': 'U', - '\u0216': 'U', - '\u01AF': 'U', - '\u1EEA': 'U', - '\u1EE8': 'U', - '\u1EEE': 'U', - '\u1EEC': 'U', - '\u1EF0': 'U', - '\u1EE4': 'U', - '\u1E72': 'U', - '\u0172': 'U', - '\u1E76': 'U', - '\u1E74': 'U', - '\u0244': 'U', - '\u24CB': 'V', - '\uFF36': 'V', - '\u1E7C': 'V', - '\u1E7E': 'V', - '\u01B2': 'V', - '\uA75E': 'V', - '\u0245': 'V', - '\uA760': 'VY', - '\u24CC': 'W', - '\uFF37': 'W', - '\u1E80': 'W', - '\u1E82': 'W', - '\u0174': 'W', - '\u1E86': 'W', - '\u1E84': 'W', - '\u1E88': 'W', - '\u2C72': 'W', - '\u24CD': 'X', - '\uFF38': 'X', - '\u1E8A': 'X', - '\u1E8C': 'X', - '\u24CE': 'Y', - '\uFF39': 'Y', - '\u1EF2': 'Y', - '\u00DD': 'Y', - '\u0176': 'Y', - '\u1EF8': 'Y', - '\u0232': 'Y', - '\u1E8E': 'Y', - '\u0178': 'Y', - '\u1EF6': 'Y', - '\u1EF4': 'Y', - '\u01B3': 'Y', - '\u024E': 'Y', - '\u1EFE': 'Y', - '\u24CF': 'Z', - '\uFF3A': 'Z', - '\u0179': 'Z', - '\u1E90': 'Z', - '\u017B': 'Z', - '\u017D': 'Z', - '\u1E92': 'Z', - '\u1E94': 'Z', - '\u01B5': 'Z', - '\u0224': 'Z', - '\u2C7F': 'Z', - '\u2C6B': 'Z', - '\uA762': 'Z', - '\u24D0': 'a', - '\uFF41': 'a', - '\u1E9A': 'a', - '\u00E0': 'a', - '\u00E1': 'a', - '\u00E2': 'a', - '\u1EA7': 'a', - '\u1EA5': 'a', - '\u1EAB': 'a', - '\u1EA9': 'a', - '\u00E3': 'a', - '\u0101': 'a', - '\u0103': 'a', - '\u1EB1': 'a', - '\u1EAF': 'a', - '\u1EB5': 'a', - '\u1EB3': 'a', - '\u0227': 'a', - '\u01E1': 'a', - '\u00E4': 'a', - '\u01DF': 'a', - '\u1EA3': 'a', - '\u00E5': 'a', - '\u01FB': 'a', - '\u01CE': 'a', - '\u0201': 'a', - '\u0203': 'a', - '\u1EA1': 'a', - '\u1EAD': 'a', - '\u1EB7': 'a', - '\u1E01': 'a', - '\u0105': 'a', - '\u2C65': 'a', - '\u0250': 'a', - '\uA733': 'aa', - '\u00E6': 'ae', - '\u01FD': 'ae', - '\u01E3': 'ae', - '\uA735': 'ao', - '\uA737': 'au', - '\uA739': 'av', - '\uA73B': 'av', - '\uA73D': 'ay', - '\u24D1': 'b', - '\uFF42': 'b', - '\u1E03': 'b', - '\u1E05': 'b', - '\u1E07': 'b', - '\u0180': 'b', - '\u0183': 'b', - '\u0253': 'b', - '\u24D2': 'c', - '\uFF43': 'c', - '\u0107': 'c', - '\u0109': 'c', - '\u010B': 'c', - '\u010D': 'c', - '\u00E7': 'c', - '\u1E09': 'c', - '\u0188': 'c', - '\u023C': 'c', - '\uA73F': 'c', - '\u2184': 'c', - '\u24D3': 'd', - '\uFF44': 'd', - '\u1E0B': 'd', - '\u010F': 'd', - '\u1E0D': 'd', - '\u1E11': 'd', - '\u1E13': 'd', - '\u1E0F': 'd', - '\u0111': 'd', - '\u018C': 'd', - '\u0256': 'd', - '\u0257': 'd', - '\uA77A': 'd', - '\u01F3': 'dz', - '\u01C6': 'dz', - '\u24D4': 'e', - '\uFF45': 'e', - '\u00E8': 'e', - '\u00E9': 'e', - '\u00EA': 'e', - '\u1EC1': 'e', - '\u1EBF': 'e', - '\u1EC5': 'e', - '\u1EC3': 'e', - '\u1EBD': 'e', - '\u0113': 'e', - '\u1E15': 'e', - '\u1E17': 'e', - '\u0115': 'e', - '\u0117': 'e', - '\u00EB': 'e', - '\u1EBB': 'e', - '\u011B': 'e', - '\u0205': 'e', - '\u0207': 'e', - '\u1EB9': 'e', - '\u1EC7': 'e', - '\u0229': 'e', - '\u1E1D': 'e', - '\u0119': 'e', - '\u1E19': 'e', - '\u1E1B': 'e', - '\u0247': 'e', - '\u025B': 'e', - '\u01DD': 'e', - '\u24D5': 'f', - '\uFF46': 'f', - '\u1E1F': 'f', - '\u0192': 'f', - '\uA77C': 'f', - '\u24D6': 'g', - '\uFF47': 'g', - '\u01F5': 'g', - '\u011D': 'g', - '\u1E21': 'g', - '\u011F': 'g', - '\u0121': 'g', - '\u01E7': 'g', - '\u0123': 'g', - '\u01E5': 'g', - '\u0260': 'g', - '\uA7A1': 'g', - '\u1D79': 'g', - '\uA77F': 'g', - '\u24D7': 'h', - '\uFF48': 'h', - '\u0125': 'h', - '\u1E23': 'h', - '\u1E27': 'h', - '\u021F': 'h', - '\u1E25': 'h', - '\u1E29': 'h', - '\u1E2B': 'h', - '\u1E96': 'h', - '\u0127': 'h', - '\u2C68': 'h', - '\u2C76': 'h', - '\u0265': 'h', - '\u0195': 'hv', - '\u24D8': 'i', - '\uFF49': 'i', - '\u00EC': 'i', - '\u00ED': 'i', - '\u00EE': 'i', - '\u0129': 'i', - '\u012B': 'i', - '\u012D': 'i', - '\u00EF': 'i', - '\u1E2F': 'i', - '\u1EC9': 'i', - '\u01D0': 'i', - '\u0209': 'i', - '\u020B': 'i', - '\u1ECB': 'i', - '\u012F': 'i', - '\u1E2D': 'i', - '\u0268': 'i', - '\u0131': 'i', - '\u24D9': 'j', - '\uFF4A': 'j', - '\u0135': 'j', - '\u01F0': 'j', - '\u0249': 'j', - '\u24DA': 'k', - '\uFF4B': 'k', - '\u1E31': 'k', - '\u01E9': 'k', - '\u1E33': 'k', - '\u0137': 'k', - '\u1E35': 'k', - '\u0199': 'k', - '\u2C6A': 'k', - '\uA741': 'k', - '\uA743': 'k', - '\uA745': 'k', - '\uA7A3': 'k', - '\u24DB': 'l', - '\uFF4C': 'l', - '\u0140': 'l', - '\u013A': 'l', - '\u013E': 'l', - '\u1E37': 'l', - '\u1E39': 'l', - '\u013C': 'l', - '\u1E3D': 'l', - '\u1E3B': 'l', - '\u017F': 'l', - '\u0142': 'l', - '\u019A': 'l', - '\u026B': 'l', - '\u2C61': 'l', - '\uA749': 'l', - '\uA781': 'l', - '\uA747': 'l', - '\u01C9': 'lj', - '\u24DC': 'm', - '\uFF4D': 'm', - '\u1E3F': 'm', - '\u1E41': 'm', - '\u1E43': 'm', - '\u0271': 'm', - '\u026F': 'm', - '\u24DD': 'n', - '\uFF4E': 'n', - '\u01F9': 'n', - '\u0144': 'n', - '\u00F1': 'n', - '\u1E45': 'n', - '\u0148': 'n', - '\u1E47': 'n', - '\u0146': 'n', - '\u1E4B': 'n', - '\u1E49': 'n', - '\u019E': 'n', - '\u0272': 'n', - '\u0149': 'n', - '\uA791': 'n', - '\uA7A5': 'n', - '\u01CC': 'nj', - '\u24DE': 'o', - '\uFF4F': 'o', - '\u00F2': 'o', - '\u00F3': 'o', - '\u00F4': 'o', - '\u1ED3': 'o', - '\u1ED1': 'o', - '\u1ED7': 'o', - '\u1ED5': 'o', - '\u00F5': 'o', - '\u1E4D': 'o', - '\u022D': 'o', - '\u1E4F': 'o', - '\u014D': 'o', - '\u1E51': 'o', - '\u1E53': 'o', - '\u014F': 'o', - '\u022F': 'o', - '\u0231': 'o', - '\u00F6': 'o', - '\u022B': 'o', - '\u1ECF': 'o', - '\u0151': 'o', - '\u01D2': 'o', - '\u020D': 'o', - '\u020F': 'o', - '\u01A1': 'o', - '\u1EDD': 'o', - '\u1EDB': 'o', - '\u1EE1': 'o', - '\u1EDF': 'o', - '\u1EE3': 'o', - '\u1ECD': 'o', - '\u1ED9': 'o', - '\u01EB': 'o', - '\u01ED': 'o', - '\u00F8': 'o', - '\u01FF': 'o', - '\u0254': 'o', - '\uA74B': 'o', - '\uA74D': 'o', - '\u0275': 'o', - '\u0153': 'oe', - '\u01A3': 'oi', - '\u0223': 'ou', - '\uA74F': 'oo', - '\u24DF': 'p', - '\uFF50': 'p', - '\u1E55': 'p', - '\u1E57': 'p', - '\u01A5': 'p', - '\u1D7D': 'p', - '\uA751': 'p', - '\uA753': 'p', - '\uA755': 'p', - '\u24E0': 'q', - '\uFF51': 'q', - '\u024B': 'q', - '\uA757': 'q', - '\uA759': 'q', - '\u24E1': 'r', - '\uFF52': 'r', - '\u0155': 'r', - '\u1E59': 'r', - '\u0159': 'r', - '\u0211': 'r', - '\u0213': 'r', - '\u1E5B': 'r', - '\u1E5D': 'r', - '\u0157': 'r', - '\u1E5F': 'r', - '\u024D': 'r', - '\u027D': 'r', - '\uA75B': 'r', - '\uA7A7': 'r', - '\uA783': 'r', - '\u24E2': 's', - '\uFF53': 's', - '\u00DF': 's', - '\u015B': 's', - '\u1E65': 's', - '\u015D': 's', - '\u1E61': 's', - '\u0161': 's', - '\u1E67': 's', - '\u1E63': 's', - '\u1E69': 's', - '\u0219': 's', - '\u015F': 's', - '\u023F': 's', - '\uA7A9': 's', - '\uA785': 's', - '\u1E9B': 's', - '\u24E3': 't', - '\uFF54': 't', - '\u1E6B': 't', - '\u1E97': 't', - '\u0165': 't', - '\u1E6D': 't', - '\u021B': 't', - '\u0163': 't', - '\u1E71': 't', - '\u1E6F': 't', - '\u0167': 't', - '\u01AD': 't', - '\u0288': 't', - '\u2C66': 't', - '\uA787': 't', - '\uA729': 'tz', - '\u24E4': 'u', - '\uFF55': 'u', - '\u00F9': 'u', - '\u00FA': 'u', - '\u00FB': 'u', - '\u0169': 'u', - '\u1E79': 'u', - '\u016B': 'u', - '\u1E7B': 'u', - '\u016D': 'u', - '\u00FC': 'u', - '\u01DC': 'u', - '\u01D8': 'u', - '\u01D6': 'u', - '\u01DA': 'u', - '\u1EE7': 'u', - '\u016F': 'u', - '\u0171': 'u', - '\u01D4': 'u', - '\u0215': 'u', - '\u0217': 'u', - '\u01B0': 'u', - '\u1EEB': 'u', - '\u1EE9': 'u', - '\u1EEF': 'u', - '\u1EED': 'u', - '\u1EF1': 'u', - '\u1EE5': 'u', - '\u1E73': 'u', - '\u0173': 'u', - '\u1E77': 'u', - '\u1E75': 'u', - '\u0289': 'u', - '\u24E5': 'v', - '\uFF56': 'v', - '\u1E7D': 'v', - '\u1E7F': 'v', - '\u028B': 'v', - '\uA75F': 'v', - '\u028C': 'v', - '\uA761': 'vy', - '\u24E6': 'w', - '\uFF57': 'w', - '\u1E81': 'w', - '\u1E83': 'w', - '\u0175': 'w', - '\u1E87': 'w', - '\u1E85': 'w', - '\u1E98': 'w', - '\u1E89': 'w', - '\u2C73': 'w', - '\u24E7': 'x', - '\uFF58': 'x', - '\u1E8B': 'x', - '\u1E8D': 'x', - '\u24E8': 'y', - '\uFF59': 'y', - '\u1EF3': 'y', - '\u00FD': 'y', - '\u0177': 'y', - '\u1EF9': 'y', - '\u0233': 'y', - '\u1E8F': 'y', - '\u00FF': 'y', - '\u1EF7': 'y', - '\u1E99': 'y', - '\u1EF5': 'y', - '\u01B4': 'y', - '\u024F': 'y', - '\u1EFF': 'y', - '\u24E9': 'z', - '\uFF5A': 'z', - '\u017A': 'z', - '\u1E91': 'z', - '\u017C': 'z', - '\u017E': 'z', - '\u1E93': 'z', - '\u1E95': 'z', - '\u01B6': 'z', - '\u0225': 'z', - '\u0240': 'z', - '\u2C6C': 'z', - '\uA763': 'z', - '\u0386': '\u0391', - '\u0388': '\u0395', - '\u0389': '\u0397', - '\u038A': '\u0399', - '\u03AA': '\u0399', - '\u038C': '\u039F', - '\u038E': '\u03A5', - '\u03AB': '\u03A5', - '\u038F': '\u03A9', - '\u03AC': '\u03B1', - '\u03AD': '\u03B5', - '\u03AE': '\u03B7', - '\u03AF': '\u03B9', - '\u03CA': '\u03B9', - '\u0390': '\u03B9', - '\u03CC': '\u03BF', - '\u03CD': '\u03C5', - '\u03CB': '\u03C5', - '\u03B0': '\u03C5', - '\u03CE': '\u03C9', - '\u03C2': '\u03C3', - '\u2019': '\'' - }; - - return diacritics; - }); - - S2.define('select2/data/base',[ - '../utils' - ], function (Utils) { - function BaseAdapter ($element, options) { - BaseAdapter.__super__.constructor.call(this); - } - - Utils.Extend(BaseAdapter, Utils.Observable); - - BaseAdapter.prototype.current = function (callback) { - throw new Error('The `current` method must be defined in child classes.'); - }; - - BaseAdapter.prototype.query = function (params, callback) { - throw new Error('The `query` method must be defined in child classes.'); - }; - - BaseAdapter.prototype.bind = function (container, $container) { - // Can be implemented in subclasses - }; - - BaseAdapter.prototype.destroy = function () { - // Can be implemented in subclasses - }; - - BaseAdapter.prototype.generateResultId = function (container, data) { - var id = container.id + '-result-'; - - id += Utils.generateChars(4); - - if (data.id != null) { - id += '-' + data.id.toString(); - } else { - id += '-' + Utils.generateChars(4); - } - return id; - }; - - return BaseAdapter; - }); - - S2.define('select2/data/select',[ - './base', - '../utils', - 'jquery' - ], function (BaseAdapter, Utils, $) { - function SelectAdapter ($element, options) { - this.$element = $element; - this.options = options; - - SelectAdapter.__super__.constructor.call(this); - } - - Utils.Extend(SelectAdapter, BaseAdapter); - - SelectAdapter.prototype.current = function (callback) { - var self = this; - - var data = Array.prototype.map.call( - this.$element[0].querySelectorAll(':checked'), - function (selectedElement) { - return self.item($(selectedElement)); - } - ); - - callback(data); - }; - - SelectAdapter.prototype.select = function (data) { - var self = this; - - data.selected = true; - - // If data.element is a DOM node, use it instead - if ( - data.element != null && data.element.tagName.toLowerCase() === 'option' - ) { - data.element.selected = true; - - this.$element.trigger('input').trigger('change'); - - return; - } - - if (this.$element.prop('multiple')) { - this.current(function (currentData) { - var val = []; - - data = [data]; - data.push.apply(data, currentData); - - for (var d = 0; d < data.length; d++) { - var id = data[d].id; - - if (val.indexOf(id) === -1) { - val.push(id); - } - } - - self.$element.val(val); - self.$element.trigger('input').trigger('change'); - }); - } else { - var val = data.id; - - this.$element.val(val); - this.$element.trigger('input').trigger('change'); - } - }; - - SelectAdapter.prototype.unselect = function (data) { - var self = this; - - if (!this.$element.prop('multiple')) { - return; - } - - data.selected = false; - - if ( - data.element != null && - data.element.tagName.toLowerCase() === 'option' - ) { - data.element.selected = false; - - this.$element.trigger('input').trigger('change'); - - return; - } - - this.current(function (currentData) { - var val = []; - - for (var d = 0; d < currentData.length; d++) { - var id = currentData[d].id; - - if (id !== data.id && val.indexOf(id) === -1) { - val.push(id); - } - } - - self.$element.val(val); - - self.$element.trigger('input').trigger('change'); - }); - }; - - SelectAdapter.prototype.bind = function (container, $container) { - var self = this; - - this.container = container; - - container.on('select', function (params) { - self.select(params.data); - }); - - container.on('unselect', function (params) { - self.unselect(params.data); - }); - }; - - SelectAdapter.prototype.destroy = function () { - // Remove anything added to child elements - this.$element.find('*').each(function () { - // Remove any custom data set by Select2 - Utils.RemoveData(this); - }); - }; - - SelectAdapter.prototype.query = function (params, callback) { - var data = []; - var self = this; - - var $options = this.$element.children(); - - $options.each(function () { - if ( - this.tagName.toLowerCase() !== 'option' && - this.tagName.toLowerCase() !== 'optgroup' - ) { - return; - } - - var $option = $(this); - - var option = self.item($option); - - var matches = self.matches(params, option); - - if (matches !== null) { - data.push(matches); - } - }); - - callback({ - results: data - }); - }; - - SelectAdapter.prototype.addOptions = function ($options) { - this.$element.append($options); - }; - - SelectAdapter.prototype.option = function (data) { - var option; - - if (data.children) { - option = document.createElement('optgroup'); - option.label = data.text; - } else { - option = document.createElement('option'); - - if (option.textContent !== undefined) { - option.textContent = data.text; - } else { - option.innerText = data.text; - } - } - - if (data.id !== undefined) { - option.value = data.id; - } - - if (data.disabled) { - option.disabled = true; - } - - if (data.selected) { - option.selected = true; - } - - if (data.title) { - option.title = data.title; - } - - var normalizedData = this._normalizeItem(data); - normalizedData.element = option; - - // Override the option's data with the combined data - Utils.StoreData(option, 'data', normalizedData); - - return $(option); - }; - - SelectAdapter.prototype.item = function ($option) { - var data = {}; - - data = Utils.GetData($option[0], 'data'); - - if (data != null) { - return data; - } - - var option = $option[0]; - - if (option.tagName.toLowerCase() === 'option') { - data = { - id: $option.val(), - text: $option.text(), - disabled: $option.prop('disabled'), - selected: $option.prop('selected'), - title: $option.prop('title') - }; - } else if (option.tagName.toLowerCase() === 'optgroup') { - data = { - text: $option.prop('label'), - children: [], - title: $option.prop('title') - }; - - var $children = $option.children('option'); - var children = []; - - for (var c = 0; c < $children.length; c++) { - var $child = $($children[c]); - - var child = this.item($child); - - children.push(child); - } - - data.children = children; - } - - data = this._normalizeItem(data); - data.element = $option[0]; - - Utils.StoreData($option[0], 'data', data); - - return data; - }; - - SelectAdapter.prototype._normalizeItem = function (item) { - if (item !== Object(item)) { - item = { - id: item, - text: item - }; - } - - item = $.extend({}, { - text: '' - }, item); - - var defaults = { - selected: false, - disabled: false - }; - - if (item.id != null) { - item.id = item.id.toString(); - } - - if (item.text != null) { - item.text = item.text.toString(); - } - - if (item._resultId == null && item.id && this.container != null) { - item._resultId = this.generateResultId(this.container, item); - } - - return $.extend({}, defaults, item); - }; - - SelectAdapter.prototype.matches = function (params, data) { - var matcher = this.options.get('matcher'); - - return matcher(params, data); - }; - - return SelectAdapter; - }); - - S2.define('select2/data/array',[ - './select', - '../utils', - 'jquery' - ], function (SelectAdapter, Utils, $) { - function ArrayAdapter ($element, options) { - this._dataToConvert = options.get('data') || []; - - ArrayAdapter.__super__.constructor.call(this, $element, options); - } - - Utils.Extend(ArrayAdapter, SelectAdapter); - - ArrayAdapter.prototype.bind = function (container, $container) { - ArrayAdapter.__super__.bind.call(this, container, $container); - - this.addOptions(this.convertToOptions(this._dataToConvert)); - }; - - ArrayAdapter.prototype.select = function (data) { - var $option = this.$element.find('option').filter(function (i, elm) { - return elm.value == data.id.toString(); - }); - - if ($option.length === 0) { - $option = this.option(data); - - this.addOptions($option); - } - - ArrayAdapter.__super__.select.call(this, data); - }; - - ArrayAdapter.prototype.convertToOptions = function (data) { - var self = this; - - var $existing = this.$element.find('option'); - var existingIds = $existing.map(function () { - return self.item($(this)).id; - }).get(); - - var $options = []; - - // Filter out all items except for the one passed in the argument - function onlyItem (item) { - return function () { - return $(this).val() == item.id; - }; - } - - for (var d = 0; d < data.length; d++) { - var item = this._normalizeItem(data[d]); - - // Skip items which were pre-loaded, only merge the data - if (existingIds.indexOf(item.id) >= 0) { - var $existingOption = $existing.filter(onlyItem(item)); - - var existingData = this.item($existingOption); - var newData = $.extend(true, {}, item, existingData); - - var $newOption = this.option(newData); - - $existingOption.replaceWith($newOption); - - continue; - } - - var $option = this.option(item); - - if (item.children) { - var $children = this.convertToOptions(item.children); - - $option.append($children); - } - - $options.push($option); - } - - return $options; - }; - - return ArrayAdapter; - }); - - S2.define('select2/data/ajax',[ - './array', - '../utils', - 'jquery' - ], function (ArrayAdapter, Utils, $) { - function AjaxAdapter ($element, options) { - this.ajaxOptions = this._applyDefaults(options.get('ajax')); - - if (this.ajaxOptions.processResults != null) { - this.processResults = this.ajaxOptions.processResults; - } - - AjaxAdapter.__super__.constructor.call(this, $element, options); - } - - Utils.Extend(AjaxAdapter, ArrayAdapter); - - AjaxAdapter.prototype._applyDefaults = function (options) { - var defaults = { - data: function (params) { - return $.extend({}, params, { - q: params.term - }); - }, - transport: function (params, success, failure) { - var $request = $.ajax(params); - - $request.then(success); - $request.fail(failure); - - return $request; - } - }; - - return $.extend({}, defaults, options, true); - }; - - AjaxAdapter.prototype.processResults = function (results) { - return results; - }; - - AjaxAdapter.prototype.query = function (params, callback) { - var matches = []; - var self = this; - - if (this._request != null) { - // JSONP requests cannot always be aborted - if ($.isFunction(this._request.abort)) { - this._request.abort(); - } - - this._request = null; - } - - var options = $.extend({ - type: 'GET' - }, this.ajaxOptions); - - if (typeof options.url === 'function') { - options.url = options.url.call(this.$element, params); - } - - if (typeof options.data === 'function') { - options.data = options.data.call(this.$element, params); - } - - function request () { - var $request = options.transport(options, function (data) { - var results = self.processResults(data, params); - - if (self.options.get('debug') && window.console && console.error) { - // Check to make sure that the response included a `results` key. - if (!results || !results.results || !Array.isArray(results.results)) { - console.error( - 'Select2: The AJAX results did not return an array in the ' + - '`results` key of the response.' - ); - } - } - - callback(results); - }, function () { - // Attempt to detect if a request was aborted - // Only works if the transport exposes a status property - if ('status' in $request && - ($request.status === 0 || $request.status === '0')) { - return; - } - - self.trigger('results:message', { - message: 'errorLoading' - }); - }); - - self._request = $request; - } - - if (this.ajaxOptions.delay && params.term != null) { - if (this._queryTimeout) { - window.clearTimeout(this._queryTimeout); - } - - this._queryTimeout = window.setTimeout(request, this.ajaxOptions.delay); - } else { - request(); - } - }; - - return AjaxAdapter; - }); - - S2.define('select2/data/tags',[ - 'jquery' - ], function ($) { - function Tags (decorated, $element, options) { - var tags = options.get('tags'); - - var createTag = options.get('createTag'); - - if (createTag !== undefined) { - this.createTag = createTag; - } - - var insertTag = options.get('insertTag'); - - if (insertTag !== undefined) { - this.insertTag = insertTag; - } - - decorated.call(this, $element, options); - - if (Array.isArray(tags)) { - for (var t = 0; t < tags.length; t++) { - var tag = tags[t]; - var item = this._normalizeItem(tag); - - var $option = this.option(item); - - this.$element.append($option); - } - } - } - - Tags.prototype.query = function (decorated, params, callback) { - var self = this; - - this._removeOldTags(); - - if (params.term == null || params.page != null) { - decorated.call(this, params, callback); - return; - } - - function wrapper (obj, child) { - var data = obj.results; - - for (var i = 0; i < data.length; i++) { - var option = data[i]; - - var checkChildren = ( - option.children != null && - !wrapper({ - results: option.children - }, true) - ); - - var optionText = (option.text || '').toUpperCase(); - var paramsTerm = (params.term || '').toUpperCase(); - - var checkText = optionText === paramsTerm; - - if (checkText || checkChildren) { - if (child) { - return false; - } - - obj.data = data; - callback(obj); - - return; - } - } - - if (child) { - return true; - } - - var tag = self.createTag(params); - - if (tag != null) { - var $option = self.option(tag); - $option.attr('data-select2-tag', true); - - self.addOptions([$option]); - - self.insertTag(data, tag); - } - - obj.results = data; - - callback(obj); - } - - decorated.call(this, params, wrapper); - }; - - Tags.prototype.createTag = function (decorated, params) { - if (params.term == null) { - return null; - } - - var term = params.term.trim(); - - if (term === '') { - return null; - } - - return { - id: term, - text: term - }; - }; - - Tags.prototype.insertTag = function (_, data, tag) { - data.unshift(tag); - }; - - Tags.prototype._removeOldTags = function (_) { - var $options = this.$element.find('option[data-select2-tag]'); - - $options.each(function () { - if (this.selected) { - return; - } - - $(this).remove(); - }); - }; - - return Tags; - }); - - S2.define('select2/data/tokenizer',[ - 'jquery' - ], function ($) { - function Tokenizer (decorated, $element, options) { - var tokenizer = options.get('tokenizer'); - - if (tokenizer !== undefined) { - this.tokenizer = tokenizer; - } - - decorated.call(this, $element, options); - } - - Tokenizer.prototype.bind = function (decorated, container, $container) { - decorated.call(this, container, $container); - - this.$search = container.dropdown.$search || container.selection.$search || - $container.find('.select2-search__field'); - }; - - Tokenizer.prototype.query = function (decorated, params, callback) { - var self = this; - - function createAndSelect (data) { - // Normalize the data object so we can use it for checks - var item = self._normalizeItem(data); - - // Check if the data object already exists as a tag - // Select it if it doesn't - var $existingOptions = self.$element.find('option').filter(function () { - return $(this).val() === item.id; - }); - - // If an existing option wasn't found for it, create the option - if (!$existingOptions.length) { - var $option = self.option(item); - $option.attr('data-select2-tag', true); - - self._removeOldTags(); - self.addOptions([$option]); - } - - // Select the item, now that we know there is an option for it - select(item); - } - - function select (data) { - self.trigger('select', { - data: data - }); - } - - params.term = params.term || ''; - - var tokenData = this.tokenizer(params, this.options, createAndSelect); - - if (tokenData.term !== params.term) { - // Replace the search term if we have the search box - if (this.$search.length) { - this.$search.val(tokenData.term); - this.$search.trigger('focus'); - } - - params.term = tokenData.term; - } - - decorated.call(this, params, callback); - }; - - Tokenizer.prototype.tokenizer = function (_, params, options, callback) { - var separators = options.get('tokenSeparators') || []; - var term = params.term; - var i = 0; - - var createTag = this.createTag || function (params) { - return { - id: params.term, - text: params.term - }; - }; - - while (i < term.length) { - var termChar = term[i]; - - if (separators.indexOf(termChar) === -1) { - i++; - - continue; - } - - var part = term.substr(0, i); - var partParams = $.extend({}, params, { - term: part - }); - - var data = createTag(partParams); - - if (data == null) { - i++; - continue; - } - - callback(data); - - // Reset the term to not include the tokenized portion - term = term.substr(i + 1) || ''; - i = 0; - } - - return { - term: term - }; - }; - - return Tokenizer; - }); - - S2.define('select2/data/minimumInputLength',[ - - ], function () { - function MinimumInputLength (decorated, $e, options) { - this.minimumInputLength = options.get('minimumInputLength'); - - decorated.call(this, $e, options); - } - - MinimumInputLength.prototype.query = function (decorated, params, callback) { - params.term = params.term || ''; - - if (params.term.length < this.minimumInputLength) { - this.trigger('results:message', { - message: 'inputTooShort', - args: { - minimum: this.minimumInputLength, - input: params.term, - params: params - } - }); - - return; - } - - decorated.call(this, params, callback); - }; - - return MinimumInputLength; - }); - - S2.define('select2/data/maximumInputLength',[ - - ], function () { - function MaximumInputLength (decorated, $e, options) { - this.maximumInputLength = options.get('maximumInputLength'); - - decorated.call(this, $e, options); - } - - MaximumInputLength.prototype.query = function (decorated, params, callback) { - params.term = params.term || ''; - - if (this.maximumInputLength > 0 && - params.term.length > this.maximumInputLength) { - this.trigger('results:message', { - message: 'inputTooLong', - args: { - maximum: this.maximumInputLength, - input: params.term, - params: params - } - }); - - return; - } - - decorated.call(this, params, callback); - }; - - return MaximumInputLength; - }); - - S2.define('select2/data/maximumSelectionLength',[ - - ], function (){ - function MaximumSelectionLength (decorated, $e, options) { - this.maximumSelectionLength = options.get('maximumSelectionLength'); - - decorated.call(this, $e, options); - } - - MaximumSelectionLength.prototype.bind = - function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('select', function () { - self._checkIfMaximumSelected(); - }); - }; - - MaximumSelectionLength.prototype.query = - function (decorated, params, callback) { - var self = this; - - this._checkIfMaximumSelected(function () { - decorated.call(self, params, callback); - }); - }; - - MaximumSelectionLength.prototype._checkIfMaximumSelected = - function (_, successCallback) { - var self = this; - - this.current(function (currentData) { - var count = currentData != null ? currentData.length : 0; - if (self.maximumSelectionLength > 0 && - count >= self.maximumSelectionLength) { - self.trigger('results:message', { - message: 'maximumSelected', - args: { - maximum: self.maximumSelectionLength - } - }); - return; - } - - if (successCallback) { - successCallback(); - } - }); - }; - - return MaximumSelectionLength; - }); - - S2.define('select2/dropdown',[ - 'jquery', - './utils' - ], function ($, Utils) { - function Dropdown ($element, options) { - this.$element = $element; - this.options = options; - - Dropdown.__super__.constructor.call(this); - } - - Utils.Extend(Dropdown, Utils.Observable); - - Dropdown.prototype.render = function () { - var $dropdown = $( - '' + - '' + - '' - ); - - $dropdown.attr('dir', this.options.get('dir')); - - this.$dropdown = $dropdown; - - return $dropdown; - }; - - Dropdown.prototype.bind = function () { - // Should be implemented in subclasses - }; - - Dropdown.prototype.position = function ($dropdown, $container) { - // Should be implemented in subclasses - }; - - Dropdown.prototype.destroy = function () { - // Remove the dropdown from the DOM - this.$dropdown.remove(); - }; - - return Dropdown; - }); - - S2.define('select2/dropdown/search',[ - 'jquery' - ], function ($) { - function Search () { } - - Search.prototype.render = function (decorated) { - var $rendered = decorated.call(this); - - var $search = $( - '' + - '' + - '' - ); - - this.$searchContainer = $search; - this.$search = $search.find('input'); - - this.$search.prop('autocomplete', this.options.get('autocomplete')); - - $rendered.prepend($search); - - return $rendered; - }; - - Search.prototype.bind = function (decorated, container, $container) { - var self = this; - - var resultsId = container.id + '-results'; - - decorated.call(this, container, $container); - - this.$search.on('keydown', function (evt) { - self.trigger('keypress', evt); - - self._keyUpPrevented = evt.isDefaultPrevented(); - }); - - // Workaround for browsers which do not support the `input` event - // This will prevent double-triggering of events for browsers which support - // both the `keyup` and `input` events. - this.$search.on('input', function (evt) { - // Unbind the duplicated `keyup` event - $(this).off('keyup'); - }); - - this.$search.on('keyup input', function (evt) { - self.handleSearch(evt); - }); - - container.on('open', function () { - self.$search.attr('tabindex', 0); - self.$search.attr('aria-controls', resultsId); - - self.$search.trigger('focus'); - - window.setTimeout(function () { - self.$search.trigger('focus'); - }, 0); - }); - - container.on('close', function () { - self.$search.attr('tabindex', -1); - self.$search.removeAttr('aria-controls'); - self.$search.removeAttr('aria-activedescendant'); - - self.$search.val(''); - self.$search.trigger('blur'); - }); - - container.on('focus', function () { - if (!container.isOpen()) { - self.$search.trigger('focus'); - } - }); - - container.on('results:all', function (params) { - if (params.query.term == null || params.query.term === '') { - var showSearch = self.showSearch(params); - - if (showSearch) { - self.$searchContainer[0].classList.remove('select2-search--hide'); - } else { - self.$searchContainer[0].classList.add('select2-search--hide'); - } - } - }); - - container.on('results:focus', function (params) { - if (params.data._resultId) { - self.$search.attr('aria-activedescendant', params.data._resultId); - } else { - self.$search.removeAttr('aria-activedescendant'); - } - }); - }; - - Search.prototype.handleSearch = function (evt) { - if (!this._keyUpPrevented) { - var input = this.$search.val(); - - this.trigger('query', { - term: input - }); - } - - this._keyUpPrevented = false; - }; - - Search.prototype.showSearch = function (_, params) { - return true; - }; - - return Search; - }); - - S2.define('select2/dropdown/hidePlaceholder',[ - - ], function () { - function HidePlaceholder (decorated, $element, options, dataAdapter) { - this.placeholder = this.normalizePlaceholder(options.get('placeholder')); - - decorated.call(this, $element, options, dataAdapter); - } - - HidePlaceholder.prototype.append = function (decorated, data) { - data.results = this.removePlaceholder(data.results); - - decorated.call(this, data); - }; - - HidePlaceholder.prototype.normalizePlaceholder = function (_, placeholder) { - if (typeof placeholder === 'string') { - placeholder = { - id: '', - text: placeholder - }; - } - - return placeholder; - }; - - HidePlaceholder.prototype.removePlaceholder = function (_, data) { - var modifiedData = data.slice(0); - - for (var d = data.length - 1; d >= 0; d--) { - var item = data[d]; - - if (this.placeholder.id === item.id) { - modifiedData.splice(d, 1); - } - } - - return modifiedData; - }; - - return HidePlaceholder; - }); - - S2.define('select2/dropdown/infiniteScroll',[ - 'jquery' - ], function ($) { - function InfiniteScroll (decorated, $element, options, dataAdapter) { - this.lastParams = {}; - - decorated.call(this, $element, options, dataAdapter); - - this.$loadingMore = this.createLoadingMore(); - this.loading = false; - } - - InfiniteScroll.prototype.append = function (decorated, data) { - this.$loadingMore.remove(); - this.loading = false; - - decorated.call(this, data); - - if (this.showLoadingMore(data)) { - this.$results.append(this.$loadingMore); - this.loadMoreIfNeeded(); - } - }; - - InfiniteScroll.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('query', function (params) { - self.lastParams = params; - self.loading = true; - }); - - container.on('query:append', function (params) { - self.lastParams = params; - self.loading = true; - }); - - this.$results.on('scroll', this.loadMoreIfNeeded.bind(this)); - }; - - InfiniteScroll.prototype.loadMoreIfNeeded = function () { - var isLoadMoreVisible = $.contains( - document.documentElement, - this.$loadingMore[0] - ); - - if (this.loading || !isLoadMoreVisible) { - return; - } - - var currentOffset = this.$results.offset().top + - this.$results.outerHeight(false); - var loadingMoreOffset = this.$loadingMore.offset().top + - this.$loadingMore.outerHeight(false); - - if (currentOffset + 50 >= loadingMoreOffset) { - this.loadMore(); - } - }; - - InfiniteScroll.prototype.loadMore = function () { - this.loading = true; - - var params = $.extend({}, {page: 1}, this.lastParams); - - params.page++; - - this.trigger('query:append', params); - }; - - InfiniteScroll.prototype.showLoadingMore = function (_, data) { - return data.pagination && data.pagination.more; - }; - - InfiniteScroll.prototype.createLoadingMore = function () { - var $option = $( - '
      • ' - ); - - var message = this.options.get('translations').get('loadingMore'); - - $option.html(message(this.lastParams)); - - return $option; - }; - - return InfiniteScroll; - }); - - S2.define('select2/dropdown/attachBody',[ - 'jquery', - '../utils' - ], function ($, Utils) { - function AttachBody (decorated, $element, options) { - this.$dropdownParent = $(options.get('dropdownParent') || document.body); - - decorated.call(this, $element, options); - } - - AttachBody.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('open', function () { - self._showDropdown(); - self._attachPositioningHandler(container); - - // Must bind after the results handlers to ensure correct sizing - self._bindContainerResultHandlers(container); - }); - - container.on('close', function () { - self._hideDropdown(); - self._detachPositioningHandler(container); - }); - - this.$dropdownContainer.on('mousedown', function (evt) { - evt.stopPropagation(); - }); - }; - - AttachBody.prototype.destroy = function (decorated) { - decorated.call(this); - - this.$dropdownContainer.remove(); - }; - - AttachBody.prototype.position = function (decorated, $dropdown, $container) { - // Clone all of the container classes - $dropdown.attr('class', $container.attr('class')); - - $dropdown[0].classList.remove('select2'); - $dropdown[0].classList.add('select2-container--open'); - - $dropdown.css({ - position: 'absolute', - top: -999999 - }); - - this.$container = $container; - }; - - AttachBody.prototype.render = function (decorated) { - var $container = $(''); - - var $dropdown = decorated.call(this); - $container.append($dropdown); - - this.$dropdownContainer = $container; - - return $container; - }; - - AttachBody.prototype._hideDropdown = function (decorated) { - this.$dropdownContainer.detach(); - }; - - AttachBody.prototype._bindContainerResultHandlers = - function (decorated, container) { - - // These should only be bound once - if (this._containerResultsHandlersBound) { - return; - } - - var self = this; - - container.on('results:all', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('results:append', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('results:message', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('select', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('unselect', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - this._containerResultsHandlersBound = true; - }; - - AttachBody.prototype._attachPositioningHandler = - function (decorated, container) { - var self = this; - - var scrollEvent = 'scroll.select2.' + container.id; - var resizeEvent = 'resize.select2.' + container.id; - var orientationEvent = 'orientationchange.select2.' + container.id; - - var $watchers = this.$container.parents().filter(Utils.hasScroll); - $watchers.each(function () { - Utils.StoreData(this, 'select2-scroll-position', { - x: $(this).scrollLeft(), - y: $(this).scrollTop() - }); - }); - - $watchers.on(scrollEvent, function (ev) { - var position = Utils.GetData(this, 'select2-scroll-position'); - $(this).scrollTop(position.y); - }); - - $(window).on(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent, - function (e) { - self._positionDropdown(); - self._resizeDropdown(); - }); - }; - - AttachBody.prototype._detachPositioningHandler = - function (decorated, container) { - var scrollEvent = 'scroll.select2.' + container.id; - var resizeEvent = 'resize.select2.' + container.id; - var orientationEvent = 'orientationchange.select2.' + container.id; - - var $watchers = this.$container.parents().filter(Utils.hasScroll); - $watchers.off(scrollEvent); - - $(window).off(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent); - }; - - AttachBody.prototype._positionDropdown = function () { - var $window = $(window); - - var isCurrentlyAbove = this.$dropdown[0].classList - .contains('select2-dropdown--above'); - var isCurrentlyBelow = this.$dropdown[0].classList - .contains('select2-dropdown--below'); - - var newDirection = null; - - var offset = this.$container.offset(); - - offset.bottom = offset.top + this.$container.outerHeight(false); - - var container = { - height: this.$container.outerHeight(false) - }; - - container.top = offset.top; - container.bottom = offset.top + container.height; - - var dropdown = { - height: this.$dropdown.outerHeight(false) - }; - - var viewport = { - top: $window.scrollTop(), - bottom: $window.scrollTop() + $window.height() - }; - - var enoughRoomAbove = viewport.top < (offset.top - dropdown.height); - var enoughRoomBelow = viewport.bottom > (offset.bottom + dropdown.height); - - var css = { - left: offset.left, - top: container.bottom - }; - - // Determine what the parent element is to use for calculating the offset - var $offsetParent = this.$dropdownParent; - - // For statically positioned elements, we need to get the element - // that is determining the offset - if ($offsetParent.css('position') === 'static') { - $offsetParent = $offsetParent.offsetParent(); - } - - var parentOffset = { - top: 0, - left: 0 - }; - - if ( - $.contains(document.body, $offsetParent[0]) || - $offsetParent[0].isConnected - ) { - parentOffset = $offsetParent.offset(); - } - - css.top -= parentOffset.top; - css.left -= parentOffset.left; - - if (!isCurrentlyAbove && !isCurrentlyBelow) { - newDirection = 'below'; - } - - if (!enoughRoomBelow && enoughRoomAbove && !isCurrentlyAbove) { - newDirection = 'above'; - } else if (!enoughRoomAbove && enoughRoomBelow && isCurrentlyAbove) { - newDirection = 'below'; - } - - if (newDirection == 'above' || - (isCurrentlyAbove && newDirection !== 'below')) { - css.top = container.top - parentOffset.top - dropdown.height; - } - - if (newDirection != null) { - this.$dropdown[0].classList.remove('select2-dropdown--below'); - this.$dropdown[0].classList.remove('select2-dropdown--above'); - this.$dropdown[0].classList.add('select2-dropdown--' + newDirection); - - this.$container[0].classList.remove('select2-container--below'); - this.$container[0].classList.remove('select2-container--above'); - this.$container[0].classList.add('select2-container--' + newDirection); - } - - this.$dropdownContainer.css(css); - }; - - AttachBody.prototype._resizeDropdown = function () { - var css = { - width: this.$container.outerWidth(false) + 'px' - }; - - if (this.options.get('dropdownAutoWidth')) { - css.minWidth = css.width; - css.position = 'relative'; - css.width = 'auto'; - } - - this.$dropdown.css(css); - }; - - AttachBody.prototype._showDropdown = function (decorated) { - this.$dropdownContainer.appendTo(this.$dropdownParent); - - this._positionDropdown(); - this._resizeDropdown(); - }; - - return AttachBody; - }); - - S2.define('select2/dropdown/minimumResultsForSearch',[ - - ], function () { - function countResults (data) { - var count = 0; - - for (var d = 0; d < data.length; d++) { - var item = data[d]; - - if (item.children) { - count += countResults(item.children); - } else { - count++; - } - } - - return count; - } - - function MinimumResultsForSearch (decorated, $element, options, dataAdapter) { - this.minimumResultsForSearch = options.get('minimumResultsForSearch'); - - if (this.minimumResultsForSearch < 0) { - this.minimumResultsForSearch = Infinity; - } - - decorated.call(this, $element, options, dataAdapter); - } - - MinimumResultsForSearch.prototype.showSearch = function (decorated, params) { - if (countResults(params.data.results) < this.minimumResultsForSearch) { - return false; - } - - return decorated.call(this, params); - }; - - return MinimumResultsForSearch; - }); - - S2.define('select2/dropdown/selectOnClose',[ - '../utils' - ], function (Utils) { - function SelectOnClose () { } - - SelectOnClose.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('close', function (params) { - self._handleSelectOnClose(params); - }); - }; - - SelectOnClose.prototype._handleSelectOnClose = function (_, params) { - if (params && params.originalSelect2Event != null) { - var event = params.originalSelect2Event; - - // Don't select an item if the close event was triggered from a select or - // unselect event - if (event._type === 'select' || event._type === 'unselect') { - return; - } - } - - var $highlightedResults = this.getHighlightedResults(); - - // Only select highlighted results - if ($highlightedResults.length < 1) { - return; - } - - var data = Utils.GetData($highlightedResults[0], 'data'); - - // Don't re-select already selected resulte - if ( - (data.element != null && data.element.selected) || - (data.element == null && data.selected) - ) { - return; - } - - this.trigger('select', { - data: data - }); - }; - - return SelectOnClose; - }); - - S2.define('select2/dropdown/closeOnSelect',[ - - ], function () { - function CloseOnSelect () { } - - CloseOnSelect.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('select', function (evt) { - self._selectTriggered(evt); - }); - - container.on('unselect', function (evt) { - self._selectTriggered(evt); - }); - }; - - CloseOnSelect.prototype._selectTriggered = function (_, evt) { - var originalEvent = evt.originalEvent; - - // Don't close if the control key is being held - if (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)) { - return; - } - - this.trigger('close', { - originalEvent: originalEvent, - originalSelect2Event: evt - }); - }; - - return CloseOnSelect; - }); - - S2.define('select2/dropdown/dropdownCss',[ - '../utils' - ], function (Utils) { - function DropdownCSS () { } - - DropdownCSS.prototype.render = function (decorated) { - var $dropdown = decorated.call(this); - - var dropdownCssClass = this.options.get('dropdownCssClass') || ''; - - if (dropdownCssClass.indexOf(':all:') !== -1) { - dropdownCssClass = dropdownCssClass.replace(':all:', ''); - - Utils.copyNonInternalCssClasses($dropdown[0], this.$element[0]); - } - - $dropdown.addClass(dropdownCssClass); - - return $dropdown; - }; - - return DropdownCSS; - }); - - S2.define('select2/i18n/en',[],function () { - // English - return { - errorLoading: function () { - return 'The results could not be loaded.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Please delete ' + overChars + ' character'; - - if (overChars != 1) { - message += 's'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Please enter ' + remainingChars + ' or more characters'; - - return message; - }, - loadingMore: function () { - return 'Loading more results…'; - }, - maximumSelected: function (args) { - var message = 'You can only select ' + args.maximum + ' item'; - - if (args.maximum != 1) { - message += 's'; - } - - return message; - }, - noResults: function () { - return 'No results found'; - }, - searching: function () { - return 'Searching…'; - }, - removeAllItems: function () { - return 'Remove all items'; - }, - removeItem: function () { - return 'Remove item'; - } - }; - }); - - S2.define('select2/defaults',[ - 'jquery', - - './results', - - './selection/single', - './selection/multiple', - './selection/placeholder', - './selection/allowClear', - './selection/search', - './selection/selectionCss', - './selection/eventRelay', - - './utils', - './translation', - './diacritics', - - './data/select', - './data/array', - './data/ajax', - './data/tags', - './data/tokenizer', - './data/minimumInputLength', - './data/maximumInputLength', - './data/maximumSelectionLength', - - './dropdown', - './dropdown/search', - './dropdown/hidePlaceholder', - './dropdown/infiniteScroll', - './dropdown/attachBody', - './dropdown/minimumResultsForSearch', - './dropdown/selectOnClose', - './dropdown/closeOnSelect', - './dropdown/dropdownCss', - - './i18n/en' - ], function ($, - - ResultsList, - - SingleSelection, MultipleSelection, Placeholder, AllowClear, - SelectionSearch, SelectionCSS, EventRelay, - - Utils, Translation, DIACRITICS, - - SelectData, ArrayData, AjaxData, Tags, Tokenizer, - MinimumInputLength, MaximumInputLength, MaximumSelectionLength, - - Dropdown, DropdownSearch, HidePlaceholder, InfiniteScroll, - AttachBody, MinimumResultsForSearch, SelectOnClose, CloseOnSelect, - DropdownCSS, - - EnglishTranslation) { - function Defaults () { - this.reset(); - } - - Defaults.prototype.apply = function (options) { - options = $.extend(true, {}, this.defaults, options); - - if (options.dataAdapter == null) { - if (options.ajax != null) { - options.dataAdapter = AjaxData; - } else if (options.data != null) { - options.dataAdapter = ArrayData; - } else { - options.dataAdapter = SelectData; - } - - if (options.minimumInputLength > 0) { - options.dataAdapter = Utils.Decorate( - options.dataAdapter, - MinimumInputLength - ); - } - - if (options.maximumInputLength > 0) { - options.dataAdapter = Utils.Decorate( - options.dataAdapter, - MaximumInputLength - ); - } - - if (options.maximumSelectionLength > 0) { - options.dataAdapter = Utils.Decorate( - options.dataAdapter, - MaximumSelectionLength - ); - } - - if (options.tags) { - options.dataAdapter = Utils.Decorate(options.dataAdapter, Tags); - } - - if (options.tokenSeparators != null || options.tokenizer != null) { - options.dataAdapter = Utils.Decorate( - options.dataAdapter, - Tokenizer - ); - } - } - - if (options.resultsAdapter == null) { - options.resultsAdapter = ResultsList; - - if (options.ajax != null) { - options.resultsAdapter = Utils.Decorate( - options.resultsAdapter, - InfiniteScroll - ); - } - - if (options.placeholder != null) { - options.resultsAdapter = Utils.Decorate( - options.resultsAdapter, - HidePlaceholder - ); - } - - if (options.selectOnClose) { - options.resultsAdapter = Utils.Decorate( - options.resultsAdapter, - SelectOnClose - ); - } - } - - if (options.dropdownAdapter == null) { - if (options.multiple) { - options.dropdownAdapter = Dropdown; - } else { - var SearchableDropdown = Utils.Decorate(Dropdown, DropdownSearch); - - options.dropdownAdapter = SearchableDropdown; - } - - if (options.minimumResultsForSearch !== 0) { - options.dropdownAdapter = Utils.Decorate( - options.dropdownAdapter, - MinimumResultsForSearch - ); - } - - if (options.closeOnSelect) { - options.dropdownAdapter = Utils.Decorate( - options.dropdownAdapter, - CloseOnSelect - ); - } - - if (options.dropdownCssClass != null) { - options.dropdownAdapter = Utils.Decorate( - options.dropdownAdapter, - DropdownCSS - ); - } - - options.dropdownAdapter = Utils.Decorate( - options.dropdownAdapter, - AttachBody - ); - } - - if (options.selectionAdapter == null) { - if (options.multiple) { - options.selectionAdapter = MultipleSelection; - } else { - options.selectionAdapter = SingleSelection; - } - - // Add the placeholder mixin if a placeholder was specified - if (options.placeholder != null) { - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - Placeholder - ); - } - - if (options.allowClear) { - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - AllowClear - ); - } - - if (options.multiple) { - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - SelectionSearch - ); - } - - if (options.selectionCssClass != null) { - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - SelectionCSS - ); - } - - options.selectionAdapter = Utils.Decorate( - options.selectionAdapter, - EventRelay - ); - } - - // If the defaults were not previously applied from an element, it is - // possible for the language option to have not been resolved - options.language = this._resolveLanguage(options.language); - - // Always fall back to English since it will always be complete - options.language.push('en'); - - var uniqueLanguages = []; - - for (var l = 0; l < options.language.length; l++) { - var language = options.language[l]; - - if (uniqueLanguages.indexOf(language) === -1) { - uniqueLanguages.push(language); - } - } - - options.language = uniqueLanguages; - - options.translations = this._processTranslations( - options.language, - options.debug - ); - - return options; - }; - - Defaults.prototype.reset = function () { - function stripDiacritics (text) { - // Used 'uni range + named function' from http://jsperf.com/diacritics/18 - function match(a) { - return DIACRITICS[a] || a; - } - - return text.replace(/[^\u0000-\u007E]/g, match); - } - - function matcher (params, data) { - // Always return the object if there is nothing to compare - if (params.term == null || params.term.trim() === '') { - return data; - } - - // Do a recursive check for options with children - if (data.children && data.children.length > 0) { - // Clone the data object if there are children - // This is required as we modify the object to remove any non-matches - var match = $.extend(true, {}, data); - - // Check each child of the option - for (var c = data.children.length - 1; c >= 0; c--) { - var child = data.children[c]; - - var matches = matcher(params, child); - - // If there wasn't a match, remove the object in the array - if (matches == null) { - match.children.splice(c, 1); - } - } - - // If any children matched, return the new object - if (match.children.length > 0) { - return match; - } - - // If there were no matching children, check just the plain object - return matcher(params, match); - } - - var original = stripDiacritics(data.text).toUpperCase(); - var term = stripDiacritics(params.term).toUpperCase(); - - // Check if the text contains the term - if (original.indexOf(term) > -1) { - return data; - } - - // If it doesn't contain the term, don't return anything - return null; - } - - this.defaults = { - amdLanguageBase: './i18n/', - autocomplete: 'off', - closeOnSelect: true, - debug: false, - dropdownAutoWidth: false, - escapeMarkup: Utils.escapeMarkup, - language: {}, - matcher: matcher, - minimumInputLength: 0, - maximumInputLength: 0, - maximumSelectionLength: 0, - minimumResultsForSearch: 0, - selectOnClose: false, - scrollAfterSelect: false, - sorter: function (data) { - return data; - }, - templateResult: function (result) { - return result.text; - }, - templateSelection: function (selection) { - return selection.text; - }, - theme: 'default', - width: 'resolve' - }; - }; - - Defaults.prototype.applyFromElement = function (options, $element) { - var optionLanguage = options.language; - var defaultLanguage = this.defaults.language; - var elementLanguage = $element.prop('lang'); - var parentLanguage = $element.closest('[lang]').prop('lang'); - - var languages = Array.prototype.concat.call( - this._resolveLanguage(elementLanguage), - this._resolveLanguage(optionLanguage), - this._resolveLanguage(defaultLanguage), - this._resolveLanguage(parentLanguage) - ); - - options.language = languages; - - return options; - }; - - Defaults.prototype._resolveLanguage = function (language) { - if (!language) { - return []; - } - - if ($.isEmptyObject(language)) { - return []; - } - - if ($.isPlainObject(language)) { - return [language]; - } - - var languages; - - if (!Array.isArray(language)) { - languages = [language]; - } else { - languages = language; - } - - var resolvedLanguages = []; - - for (var l = 0; l < languages.length; l++) { - resolvedLanguages.push(languages[l]); - - if (typeof languages[l] === 'string' && languages[l].indexOf('-') > 0) { - // Extract the region information if it is included - var languageParts = languages[l].split('-'); - var baseLanguage = languageParts[0]; - - resolvedLanguages.push(baseLanguage); - } - } - - return resolvedLanguages; - }; - - Defaults.prototype._processTranslations = function (languages, debug) { - var translations = new Translation(); - - for (var l = 0; l < languages.length; l++) { - var languageData = new Translation(); - - var language = languages[l]; - - if (typeof language === 'string') { - try { - // Try to load it with the original name - languageData = Translation.loadPath(language); - } catch (e) { - try { - // If we couldn't load it, check if it wasn't the full path - language = this.defaults.amdLanguageBase + language; - languageData = Translation.loadPath(language); - } catch (ex) { - // The translation could not be loaded at all. Sometimes this is - // because of a configuration problem, other times this can be - // because of how Select2 helps load all possible translation files - if (debug && window.console && console.warn) { - console.warn( - 'Select2: The language file for "' + language + '" could ' + - 'not be automatically loaded. A fallback will be used instead.' - ); - } - } - } - } else if ($.isPlainObject(language)) { - languageData = new Translation(language); - } else { - languageData = language; - } - - translations.extend(languageData); - } - - return translations; - }; - - Defaults.prototype.set = function (key, value) { - var camelKey = $.camelCase(key); - - var data = {}; - data[camelKey] = value; - - var convertedData = Utils._convertData(data); - - $.extend(true, this.defaults, convertedData); - }; - - var defaults = new Defaults(); - - return defaults; - }); - - S2.define('select2/options',[ - 'jquery', - './defaults', - './utils' - ], function ($, Defaults, Utils) { - function Options (options, $element) { - this.options = options; - - if ($element != null) { - this.fromElement($element); - } - - if ($element != null) { - this.options = Defaults.applyFromElement(this.options, $element); - } - - this.options = Defaults.apply(this.options); - } - - Options.prototype.fromElement = function ($e) { - var excludedData = ['select2']; - - if (this.options.multiple == null) { - this.options.multiple = $e.prop('multiple'); - } - - if (this.options.disabled == null) { - this.options.disabled = $e.prop('disabled'); - } - - if (this.options.autocomplete == null && $e.prop('autocomplete')) { - this.options.autocomplete = $e.prop('autocomplete'); - } - - if (this.options.dir == null) { - if ($e.prop('dir')) { - this.options.dir = $e.prop('dir'); - } else if ($e.closest('[dir]').prop('dir')) { - this.options.dir = $e.closest('[dir]').prop('dir'); - } else { - this.options.dir = 'ltr'; - } - } - - $e.prop('disabled', this.options.disabled); - $e.prop('multiple', this.options.multiple); - - if (Utils.GetData($e[0], 'select2Tags')) { - if (this.options.debug && window.console && console.warn) { - console.warn( - 'Select2: The `data-select2-tags` attribute has been changed to ' + - 'use the `data-data` and `data-tags="true"` attributes and will be ' + - 'removed in future versions of Select2.' - ); - } - - Utils.StoreData($e[0], 'data', Utils.GetData($e[0], 'select2Tags')); - Utils.StoreData($e[0], 'tags', true); - } - - if (Utils.GetData($e[0], 'ajaxUrl')) { - if (this.options.debug && window.console && console.warn) { - console.warn( - 'Select2: The `data-ajax-url` attribute has been changed to ' + - '`data-ajax--url` and support for the old attribute will be removed' + - ' in future versions of Select2.' - ); - } - - $e.attr('ajax--url', Utils.GetData($e[0], 'ajaxUrl')); - Utils.StoreData($e[0], 'ajax-Url', Utils.GetData($e[0], 'ajaxUrl')); - } - - var dataset = {}; - - function upperCaseLetter(_, letter) { - return letter.toUpperCase(); - } - - // Pre-load all of the attributes which are prefixed with `data-` - for (var attr = 0; attr < $e[0].attributes.length; attr++) { - var attributeName = $e[0].attributes[attr].name; - var prefix = 'data-'; - - if (attributeName.substr(0, prefix.length) == prefix) { - // Get the contents of the attribute after `data-` - var dataName = attributeName.substring(prefix.length); - - // Get the data contents from the consistent source - // This is more than likely the jQuery data helper - var dataValue = Utils.GetData($e[0], dataName); - - // camelCase the attribute name to match the spec - var camelDataName = dataName.replace(/-([a-z])/g, upperCaseLetter); - - // Store the data attribute contents into the dataset since - dataset[camelDataName] = dataValue; - } - } - - // Prefer the element's `dataset` attribute if it exists - // jQuery 1.x does not correctly handle data attributes with multiple dashes - if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { - dataset = $.extend(true, {}, $e[0].dataset, dataset); - } - - // Prefer our internal data cache if it exists - var data = $.extend(true, {}, Utils.GetData($e[0]), dataset); - - data = Utils._convertData(data); - - for (var key in data) { - if (excludedData.indexOf(key) > -1) { - continue; - } - - if ($.isPlainObject(this.options[key])) { - $.extend(this.options[key], data[key]); - } else { - this.options[key] = data[key]; - } - } - - return this; - }; - - Options.prototype.get = function (key) { - return this.options[key]; - }; - - Options.prototype.set = function (key, val) { - this.options[key] = val; - }; - - return Options; - }); - - S2.define('select2/core',[ - 'jquery', - './options', - './utils', - './keys' - ], function ($, Options, Utils, KEYS) { - var Select2 = function ($element, options) { - if (Utils.GetData($element[0], 'select2') != null) { - Utils.GetData($element[0], 'select2').destroy(); - } - - this.$element = $element; - - this.id = this._generateId($element); - - options = options || {}; - - this.options = new Options(options, $element); - - Select2.__super__.constructor.call(this); - - // Set up the tabindex - - var tabindex = $element.attr('tabindex') || 0; - Utils.StoreData($element[0], 'old-tabindex', tabindex); - $element.attr('tabindex', '-1'); - - // Set up containers and adapters - - var DataAdapter = this.options.get('dataAdapter'); - this.dataAdapter = new DataAdapter($element, this.options); - - var $container = this.render(); - - this._placeContainer($container); - - var SelectionAdapter = this.options.get('selectionAdapter'); - this.selection = new SelectionAdapter($element, this.options); - this.$selection = this.selection.render(); - - this.selection.position(this.$selection, $container); - - var DropdownAdapter = this.options.get('dropdownAdapter'); - this.dropdown = new DropdownAdapter($element, this.options); - this.$dropdown = this.dropdown.render(); - - this.dropdown.position(this.$dropdown, $container); - - var ResultsAdapter = this.options.get('resultsAdapter'); - this.results = new ResultsAdapter($element, this.options, this.dataAdapter); - this.$results = this.results.render(); - - this.results.position(this.$results, this.$dropdown); - - // Bind events - - var self = this; - - // Bind the container to all of the adapters - this._bindAdapters(); - - // Register any DOM event handlers - this._registerDomEvents(); - - // Register any internal event handlers - this._registerDataEvents(); - this._registerSelectionEvents(); - this._registerDropdownEvents(); - this._registerResultsEvents(); - this._registerEvents(); - - // Set the initial state - this.dataAdapter.current(function (initialData) { - self.trigger('selection:update', { - data: initialData - }); - }); - - // Hide the original select - $element[0].classList.add('select2-hidden-accessible'); - $element.attr('aria-hidden', 'true'); - - // Synchronize any monitored attributes - this._syncAttributes(); - - Utils.StoreData($element[0], 'select2', this); - - // Ensure backwards compatibility with $element.data('select2'). - $element.data('select2', this); - }; - - Utils.Extend(Select2, Utils.Observable); - - Select2.prototype._generateId = function ($element) { - var id = ''; - - if ($element.attr('id') != null) { - id = $element.attr('id'); - } else if ($element.attr('name') != null) { - id = $element.attr('name') + '-' + Utils.generateChars(2); - } else { - id = Utils.generateChars(4); - } - - id = id.replace(/(:|\.|\[|\]|,)/g, ''); - id = 'select2-' + id; - - return id; - }; - - Select2.prototype._placeContainer = function ($container) { - $container.insertAfter(this.$element); - - var width = this._resolveWidth(this.$element, this.options.get('width')); - - if (width != null) { - $container.css('width', width); - } - }; - - Select2.prototype._resolveWidth = function ($element, method) { - var WIDTH = /^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i; - - if (method == 'resolve') { - var styleWidth = this._resolveWidth($element, 'style'); - - if (styleWidth != null) { - return styleWidth; - } - - return this._resolveWidth($element, 'element'); - } - - if (method == 'element') { - var elementWidth = $element.outerWidth(false); - - if (elementWidth <= 0) { - return 'auto'; - } - - return elementWidth + 'px'; - } - - if (method == 'style') { - var style = $element.attr('style'); - - if (typeof(style) !== 'string') { - return null; - } - - var attrs = style.split(';'); - - for (var i = 0, l = attrs.length; i < l; i = i + 1) { - var attr = attrs[i].replace(/\s/g, ''); - var matches = attr.match(WIDTH); - - if (matches !== null && matches.length >= 1) { - return matches[1]; - } - } - - return null; - } - - if (method == 'computedstyle') { - var computedStyle = window.getComputedStyle($element[0]); - - return computedStyle.width; - } - - return method; - }; - - Select2.prototype._bindAdapters = function () { - this.dataAdapter.bind(this, this.$container); - this.selection.bind(this, this.$container); - - this.dropdown.bind(this, this.$container); - this.results.bind(this, this.$container); - }; - - Select2.prototype._registerDomEvents = function () { - var self = this; - - this.$element.on('change.select2', function () { - self.dataAdapter.current(function (data) { - self.trigger('selection:update', { - data: data - }); - }); - }); - - this.$element.on('focus.select2', function (evt) { - self.trigger('focus', evt); - }); - - this._syncA = Utils.bind(this._syncAttributes, this); - this._syncS = Utils.bind(this._syncSubtree, this); - - this._observer = new window.MutationObserver(function (mutations) { - self._syncA(); - self._syncS(mutations); - }); - this._observer.observe(this.$element[0], { - attributes: true, - childList: true, - subtree: false - }); - }; - - Select2.prototype._registerDataEvents = function () { - var self = this; - - this.dataAdapter.on('*', function (name, params) { - self.trigger(name, params); - }); - }; - - Select2.prototype._registerSelectionEvents = function () { - var self = this; - var nonRelayEvents = ['toggle', 'focus']; - - this.selection.on('toggle', function () { - self.toggleDropdown(); - }); - - this.selection.on('focus', function (params) { - self.focus(params); - }); - - this.selection.on('*', function (name, params) { - if (nonRelayEvents.indexOf(name) !== -1) { - return; - } - - self.trigger(name, params); - }); - }; - - Select2.prototype._registerDropdownEvents = function () { - var self = this; - - this.dropdown.on('*', function (name, params) { - self.trigger(name, params); - }); - }; - - Select2.prototype._registerResultsEvents = function () { - var self = this; - - this.results.on('*', function (name, params) { - self.trigger(name, params); - }); - }; - - Select2.prototype._registerEvents = function () { - var self = this; - - this.on('open', function () { - self.$container[0].classList.add('select2-container--open'); - }); - - this.on('close', function () { - self.$container[0].classList.remove('select2-container--open'); - }); - - this.on('enable', function () { - self.$container[0].classList.remove('select2-container--disabled'); - }); - - this.on('disable', function () { - self.$container[0].classList.add('select2-container--disabled'); - }); - - this.on('blur', function () { - self.$container[0].classList.remove('select2-container--focus'); - }); - - this.on('query', function (params) { - if (!self.isOpen()) { - self.trigger('open', {}); - } - - this.dataAdapter.query(params, function (data) { - self.trigger('results:all', { - data: data, - query: params - }); - }); - }); - - this.on('query:append', function (params) { - this.dataAdapter.query(params, function (data) { - self.trigger('results:append', { - data: data, - query: params - }); - }); - }); - - this.on('keypress', function (evt) { - var key = evt.which; - - if (self.isOpen()) { - if (key === KEYS.ESC || key === KEYS.TAB || - (key === KEYS.UP && evt.altKey)) { - self.close(evt); - - evt.preventDefault(); - } else if (key === KEYS.ENTER) { - self.trigger('results:select', {}); - - evt.preventDefault(); - } else if ((key === KEYS.SPACE && evt.ctrlKey)) { - self.trigger('results:toggle', {}); - - evt.preventDefault(); - } else if (key === KEYS.UP) { - self.trigger('results:previous', {}); - - evt.preventDefault(); - } else if (key === KEYS.DOWN) { - self.trigger('results:next', {}); - - evt.preventDefault(); - } - } else { - if (key === KEYS.ENTER || key === KEYS.SPACE || - (key === KEYS.DOWN && evt.altKey)) { - self.open(); - - evt.preventDefault(); - } - } - }); - }; - - Select2.prototype._syncAttributes = function () { - this.options.set('disabled', this.$element.prop('disabled')); - - if (this.isDisabled()) { - if (this.isOpen()) { - this.close(); - } - - this.trigger('disable', {}); - } else { - this.trigger('enable', {}); - } - }; - - Select2.prototype._isChangeMutation = function (mutations) { - var self = this; - - if (mutations.addedNodes && mutations.addedNodes.length > 0) { - for (var n = 0; n < mutations.addedNodes.length; n++) { - var node = mutations.addedNodes[n]; - - if (node.selected) { - return true; - } - } - } else if (mutations.removedNodes && mutations.removedNodes.length > 0) { - return true; - } else if (Array.isArray(mutations)) { - return mutations.some(function (mutation) { - return self._isChangeMutation(mutation); - }); - } - - return false; - }; - - Select2.prototype._syncSubtree = function (mutations) { - var changed = this._isChangeMutation(mutations); - var self = this; - - // Only re-pull the data if we think there is a change - if (changed) { - this.dataAdapter.current(function (currentData) { - self.trigger('selection:update', { - data: currentData - }); - }); - } - }; - - /** - * Override the trigger method to automatically trigger pre-events when - * there are events that can be prevented. - */ - Select2.prototype.trigger = function (name, args) { - var actualTrigger = Select2.__super__.trigger; - var preTriggerMap = { - 'open': 'opening', - 'close': 'closing', - 'select': 'selecting', - 'unselect': 'unselecting', - 'clear': 'clearing' - }; - - if (args === undefined) { - args = {}; - } - - if (name in preTriggerMap) { - var preTriggerName = preTriggerMap[name]; - var preTriggerArgs = { - prevented: false, - name: name, - args: args - }; - - actualTrigger.call(this, preTriggerName, preTriggerArgs); - - if (preTriggerArgs.prevented) { - args.prevented = true; - - return; - } - } - - actualTrigger.call(this, name, args); - }; - - Select2.prototype.toggleDropdown = function () { - if (this.isDisabled()) { - return; - } - - if (this.isOpen()) { - this.close(); - } else { - this.open(); - } - }; - - Select2.prototype.open = function () { - if (this.isOpen()) { - return; - } - - if (this.isDisabled()) { - return; - } - - this.trigger('query', {}); - }; - - Select2.prototype.close = function (evt) { - if (!this.isOpen()) { - return; - } - - this.trigger('close', { originalEvent : evt }); - }; - - /** - * Helper method to abstract the "enabled" (not "disabled") state of this - * object. - * - * @return {true} if the instance is not disabled. - * @return {false} if the instance is disabled. - */ - Select2.prototype.isEnabled = function () { - return !this.isDisabled(); - }; - - /** - * Helper method to abstract the "disabled" state of this object. - * - * @return {true} if the disabled option is true. - * @return {false} if the disabled option is false. - */ - Select2.prototype.isDisabled = function () { - return this.options.get('disabled'); - }; - - Select2.prototype.isOpen = function () { - return this.$container[0].classList.contains('select2-container--open'); - }; - - Select2.prototype.hasFocus = function () { - return this.$container[0].classList.contains('select2-container--focus'); - }; - - Select2.prototype.focus = function (data) { - // No need to re-trigger focus events if we are already focused - if (this.hasFocus()) { - return; - } - - this.$container[0].classList.add('select2-container--focus'); - this.trigger('focus', {}); - }; - - Select2.prototype.enable = function (args) { - if (this.options.get('debug') && window.console && console.warn) { - console.warn( - 'Select2: The `select2("enable")` method has been deprecated and will' + - ' be removed in later Select2 versions. Use $element.prop("disabled")' + - ' instead.' - ); - } - - if (args == null || args.length === 0) { - args = [true]; - } - - var disabled = !args[0]; - - this.$element.prop('disabled', disabled); - }; - - Select2.prototype.data = function () { - if (this.options.get('debug') && - arguments.length > 0 && window.console && console.warn) { - console.warn( - 'Select2: Data can no longer be set using `select2("data")`. You ' + - 'should consider setting the value instead using `$element.val()`.' - ); - } - - var data = []; - - this.dataAdapter.current(function (currentData) { - data = currentData; - }); - - return data; - }; - - Select2.prototype.val = function (args) { - if (this.options.get('debug') && window.console && console.warn) { - console.warn( - 'Select2: The `select2("val")` method has been deprecated and will be' + - ' removed in later Select2 versions. Use $element.val() instead.' - ); - } - - if (args == null || args.length === 0) { - return this.$element.val(); - } - - var newVal = args[0]; - - if (Array.isArray(newVal)) { - newVal = newVal.map(function (obj) { - return obj.toString(); - }); - } - - this.$element.val(newVal).trigger('input').trigger('change'); - }; - - Select2.prototype.destroy = function () { - this.$container.remove(); - - this._observer.disconnect(); - this._observer = null; - - this._syncA = null; - this._syncS = null; - - this.$element.off('.select2'); - this.$element.attr('tabindex', - Utils.GetData(this.$element[0], 'old-tabindex')); - - this.$element[0].classList.remove('select2-hidden-accessible'); - this.$element.attr('aria-hidden', 'false'); - Utils.RemoveData(this.$element[0]); - this.$element.removeData('select2'); - - this.dataAdapter.destroy(); - this.selection.destroy(); - this.dropdown.destroy(); - this.results.destroy(); - - this.dataAdapter = null; - this.selection = null; - this.dropdown = null; - this.results = null; - }; - - Select2.prototype.render = function () { - var $container = $( - '' + - '' + - '' + - '' - ); - - $container.attr('dir', this.options.get('dir')); - - this.$container = $container; - - this.$container[0].classList - .add('select2-container--' + this.options.get('theme')); - - Utils.StoreData($container[0], 'element', this.$element); - - return $container; - }; - - return Select2; - }); - - S2.define('jquery-mousewheel',[ - 'jquery' - ], function ($) { - // Used to shim jQuery.mousewheel for non-full builds. - return $; - }); - - S2.define('jquery.select2',[ - 'jquery', - 'jquery-mousewheel', - - './select2/core', - './select2/defaults', - './select2/utils' - ], function ($, _, Select2, Defaults, Utils) { - if ($.fn.select2 == null) { - // All methods that should return the element - var thisMethods = ['open', 'close', 'destroy']; - - $.fn.select2 = function (options) { - options = options || {}; - - if (typeof options === 'object') { - this.each(function () { - var instanceOptions = $.extend(true, {}, options); - - var instance = new Select2($(this), instanceOptions); - }); - - return this; - } else if (typeof options === 'string') { - var ret; - var args = Array.prototype.slice.call(arguments, 1); - - this.each(function () { - var instance = Utils.GetData(this, 'select2'); - - if (instance == null && window.console && console.error) { - console.error( - 'The select2(\'' + options + '\') method was called on an ' + - 'element that is not using Select2.' - ); - } - - ret = instance[options].apply(instance, args); - }); - - // Check if we should be returning `this` - if (thisMethods.indexOf(options) > -1) { - return this; - } - - return ret; - } else { - throw new Error('Invalid arguments for Select2: ' + options); - } - }; - } - - if ($.fn.select2.defaults == null) { - $.fn.select2.defaults = Defaults; - } - - return Select2; - }); - - // Return the AMD loader configuration so it can be used outside of this file - return { - define: S2.define, - require: S2.require - }; - }()); - - // Autoload the jQuery bindings - // We know that all of the modules exist above this, so we're safe - var select2 = S2.require('jquery.select2'); - - // Hold the AMD module references on the jQuery function that was just loaded - // This allows Select2 to use the internal loader outside of this file, such - // as in the language files. - jQuery.fn.select2.amd = S2; - - // Return the Select2 instance for anyone who is importing it. - return select2; -})); diff --git a/obp-api/src/main/webapp/media/js/select2.min.js b/obp-api/src/main/webapp/media/js/select2.min.js new file mode 100644 index 000000000..fd88a10fe --- /dev/null +++ b/obp-api/src/main/webapp/media/js/select2.min.js @@ -0,0 +1 @@ +;(function(factory){if(typeof define==='function'&&define.amd){define(['jquery'],factory)}else if(typeof module==='object'&&module.exports){module.exports=function(root,jQuery){if(jQuery===undefined){if(typeof window!=='undefined'){jQuery=require('jquery')}else{jQuery=require('jquery')(root)}}factory(jQuery);return jQuery}}else{factory(jQuery)}}(function(jQuery){var S2=(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd){var S2=jQuery.fn.select2.amd}var S2;(function(){if(!S2||!S2.requirejs){if(!S2){S2={}}else{require=S2}var requirejs,require,define;(function(undef){var main,req,makeMap,handlers,defined={},waiting={},config={},defining={},hasOwn=Object.prototype.hasOwnProperty,aps=[].slice,jsSuffixRegExp=/\.js$/;function hasProp(obj,prop){return hasOwn.call(obj,prop)}function normalize(name,baseName){var nameParts,nameSegment,mapValue,foundMap,lastIndex,foundI,foundStarMap,starI,i,j,part,normalizedBaseParts,baseParts=baseName&&baseName.split("/"),map=config.map,starMap=(map&&map['*'])||{};if(name){name=name.split('/');lastIndex=name.length-1;if(config.nodeIdCompat&&jsSuffixRegExp.test(name[lastIndex])){name[lastIndex]=name[lastIndex].replace(jsSuffixRegExp,'')}if(name[0].charAt(0)==='.'&&baseParts){normalizedBaseParts=baseParts.slice(0,baseParts.length-1);name=normalizedBaseParts.concat(name)}for(i=0;i0){name.splice(i-1,2);i-=2}}}name=name.join('/')}if((baseParts||starMap)&&map){nameParts=name.split('/');for(i=nameParts.length;i>0;i-=1){nameSegment=nameParts.slice(0,i).join("/");if(baseParts){for(j=baseParts.length;j>0;j-=1){mapValue=map[baseParts.slice(0,j).join('/')];if(mapValue){mapValue=mapValue[nameSegment];if(mapValue){foundMap=mapValue;foundI=i;break}}}}if(foundMap){break}if(!foundStarMap&&starMap&&starMap[nameSegment]){foundStarMap=starMap[nameSegment];starI=i}}if(!foundMap&&foundStarMap){foundMap=foundStarMap;foundI=starI}if(foundMap){nameParts.splice(0,foundI,foundMap);name=nameParts.join('/')}}return name}function makeRequire(relName,forceSync){return function(){var args=aps.call(arguments,0);if(typeof args[0]!=='string'&&args.length===1){args.push(null)}return req.apply(undef,args.concat([relName,forceSync]))}}function makeNormalize(relName){return function(name){return normalize(name,relName)}}function makeLoad(depName){return function(value){defined[depName]=value}}function callDep(name){if(hasProp(waiting,name)){var args=waiting[name];delete waiting[name];defining[name]=true;main.apply(undef,args)}if(!hasProp(defined,name)&&!hasProp(defining,name)){throw new Error('No '+name);}return defined[name]}function splitPrefix(name){var prefix,index=name?name.indexOf('!'):-1;if(index>-1){prefix=name.substring(0,index);name=name.substring(index+1,name.length)}return[prefix,name]}function makeRelParts(relName){return relName?splitPrefix(relName):[]}makeMap=function(name,relParts){var plugin,parts=splitPrefix(name),prefix=parts[0],relResourceName=relParts[1];name=parts[1];if(prefix){prefix=normalize(prefix,relResourceName);plugin=callDep(prefix)}if(prefix){if(plugin&&plugin.normalize){name=plugin.normalize(name,makeNormalize(relResourceName))}else{name=normalize(name,relResourceName)}}else{name=normalize(name,relResourceName);parts=splitPrefix(name);prefix=parts[0];name=parts[1];if(prefix){plugin=callDep(prefix)}}return{f:prefix?prefix+'!'+name:name,n:name,pr:prefix,p:plugin}};function makeConfig(name){return function(){return(config&&config.config&&config.config[name])||{}}}handlers={require:function(name){return makeRequire(name)},exports:function(name){var e=defined[name];if(typeof e!=='undefined'){return e}else{return(defined[name]={})}},module:function(name){return{id:name,uri:'',exports:defined[name],config:makeConfig(name)}}};main=function(name,deps,callback,relName){var cjsModule,depName,ret,map,i,relParts,args=[],callbackType=typeof callback,usingExports;relName=relName||name;relParts=makeRelParts(relName);if(callbackType==='undefined'||callbackType==='function'){deps=!deps.length&&callback.length?['require','exports','module']:deps;for(i=0;i0){unshift.call(arguments,SuperClass.prototype.constructor);calledConstructor=DecoratorClass.prototype.constructor}calledConstructor.apply(this,arguments)}DecoratorClass.displayName=SuperClass.displayName;function ctr(){this.constructor=DecoratedClass}DecoratedClass.prototype=new ctr();for(var m=0;m':'>','"':'"','\'':''','/':'/'};if(typeof markup!=='string'){return markup}return String(markup).replace(/[&<>"'\/\\]/g,function(match){return replaceMap[match]})};Utils.appendMany=function($element,$nodes){if($.fn.jquery.substr(0,3)==='1.7'){var $jqNodes=$();$.map($nodes,function(node){$jqNodes=$jqNodes.add(node)});$nodes=$jqNodes}$element.append($nodes)};Utils.__cache={};var id=0;Utils.GetUniqueElementId=function(element){var select2Id=element.getAttribute('data-select2-id');if(select2Id==null){if(element.id){select2Id=element.id;element.setAttribute('data-select2-id',select2Id)}else{element.setAttribute('data-select2-id',++id);select2Id=id.toString()}}return select2Id};Utils.StoreData=function(element,name,value){var id=Utils.GetUniqueElementId(element);if(!Utils.__cache[id]){Utils.__cache[id]={}}Utils.__cache[id][name]=value};Utils.GetData=function(element,name){var id=Utils.GetUniqueElementId(element);if(name){if(Utils.__cache[id]){if(Utils.__cache[id][name]!=null){return Utils.__cache[id][name]}return $(element).data(name)}return $(element).data(name)}else{return Utils.__cache[id]}};Utils.RemoveData=function(element){var id=Utils.GetUniqueElementId(element);if(Utils.__cache[id]!=null){delete Utils.__cache[id]}element.removeAttribute('data-select2-id')};return Utils});S2.define('select2/results',['jquery','./utils'],function($,Utils){function Results($element,options,dataAdapter){this.$element=$element;this.data=dataAdapter;this.options=options;Results.__super__.constructor.call(this)}Utils.Extend(Results,Utils.Observable);Results.prototype.render=function(){var $results=$('
          ');if(this.options.get('multiple')){$results.attr('aria-multiselectable','true')}this.$results=$results;return $results};Results.prototype.clear=function(){this.$results.empty()};Results.prototype.displayMessage=function(params){var escapeMarkup=this.options.get('escapeMarkup');this.clear();this.hideLoading();var $message=$('');var message=this.options.get('translations').get(params.message);$message.append(escapeMarkup(message(params.args)));$message[0].className+=' select2-results__message';this.$results.append($message)};Results.prototype.hideMessages=function(){this.$results.find('.select2-results__message').remove()};Results.prototype.append=function(data){this.hideLoading();var $options=[];if(data.results==null||data.results.length===0){if(this.$results.children().length===0){this.trigger('results:message',{message:'noResults'})}return}data.results=this.sort(data.results);for(var d=0;d0){$selected.first().trigger('mouseenter')}else{$options.first().trigger('mouseenter')}this.ensureHighlightVisible()};Results.prototype.setClasses=function(){var self=this;this.data.current(function(selected){var selectedIds=$.map(selected,function(s){return s.id.toString()});var $options=self.$results.find('.select2-results__option[aria-selected]');$options.each(function(){var $option=$(this);var item=Utils.GetData(this,'data');var id=''+item.id;if((item.element!=null&&item.element.selected)||(item.element==null&&$.inArray(id,selectedIds)>-1)){$option.attr('aria-selected','true')}else{$option.attr('aria-selected','false')}})})};Results.prototype.showLoading=function(params){this.hideLoading();var loadingMore=this.options.get('translations').get('searching');var loading={disabled:true,loading:true,text:loadingMore(params)};var $loading=this.option(loading);$loading.className+=' loading-results';this.$results.prepend($loading)};Results.prototype.hideLoading=function(){this.$results.find('.loading-results').remove()};Results.prototype.option=function(data){var option=document.createElement('li');option.className='select2-results__option';var attrs={'role':'option','aria-selected':'false'};var matches=window.Element.prototype.matches||window.Element.prototype.msMatchesSelector||window.Element.prototype.webkitMatchesSelector;if((data.element!=null&&matches.call(data.element,':disabled'))||(data.element==null&&data.disabled)){delete attrs['aria-selected'];attrs['aria-disabled']='true'}if(data.id==null){delete attrs['aria-selected']}if(data._resultId!=null){option.id=data._resultId}if(data.title){option.title=data.title}if(data.children){attrs.role='group';attrs['aria-label']=data.text;delete attrs['aria-selected']}for(var attr in attrs){var val=attrs[attr];option.setAttribute(attr,val)}if(data.children){var $option=$(option);var label=document.createElement('strong');label.className='select2-results__group';var $label=$(label);this.template(data,label);var $children=[];for(var c=0;c',{'class':'select2-results__options select2-results__options--nested'});$childrenContainer.append($children);$option.append(label);$option.append($childrenContainer)}else{this.template(data,option)}Utils.StoreData(option,'data',data);return option};Results.prototype.bind=function(container,$container){var self=this;var id=container.id+'-results';this.$results.attr('id',id);container.on('results:all',function(params){self.clear();self.append(params.data);if(container.isOpen()){self.setClasses();self.highlightFirstItem()}});container.on('results:append',function(params){self.append(params.data);if(container.isOpen()){self.setClasses()}});container.on('query',function(params){self.hideMessages();self.showLoading(params)});container.on('select',function(){if(!container.isOpen()){return}self.setClasses();if(self.options.get('scrollAfterSelect')){self.highlightFirstItem()}});container.on('unselect',function(){if(!container.isOpen()){return}self.setClasses();if(self.options.get('scrollAfterSelect')){self.highlightFirstItem()}});container.on('open',function(){self.$results.attr('aria-expanded','true');self.$results.attr('aria-hidden','false');self.setClasses();self.ensureHighlightVisible()});container.on('close',function(){self.$results.attr('aria-expanded','false');self.$results.attr('aria-hidden','true');self.$results.removeAttr('aria-activedescendant')});container.on('results:toggle',function(){var $highlighted=self.getHighlightedResults();if($highlighted.length===0){return}$highlighted.trigger('mouseup')});container.on('results:select',function(){var $highlighted=self.getHighlightedResults();if($highlighted.length===0){return}var data=Utils.GetData($highlighted[0],'data');if($highlighted.attr('aria-selected')=='true'){self.trigger('close',{})}else{self.trigger('select',{data:data})}});container.on('results:previous',function(){var $highlighted=self.getHighlightedResults();var $options=self.$results.find('[aria-selected]');var currentIndex=$options.index($highlighted);if(currentIndex<=0){return}var nextIndex=currentIndex-1;if($highlighted.length===0){nextIndex=0}var $next=$options.eq(nextIndex);$next.trigger('mouseenter');var currentOffset=self.$results.offset().top;var nextTop=$next.offset().top;var nextOffset=self.$results.scrollTop()+(nextTop-currentOffset);if(nextIndex===0){self.$results.scrollTop(0)}else if(nextTop-currentOffset<0){self.$results.scrollTop(nextOffset)}});container.on('results:next',function(){var $highlighted=self.getHighlightedResults();var $options=self.$results.find('[aria-selected]');var currentIndex=$options.index($highlighted);var nextIndex=currentIndex+1;if(nextIndex>=$options.length){return}var $next=$options.eq(nextIndex);$next.trigger('mouseenter');var currentOffset=self.$results.offset().top+self.$results.outerHeight(false);var nextBottom=$next.offset().top+$next.outerHeight(false);var nextOffset=self.$results.scrollTop()+nextBottom-currentOffset;if(nextIndex===0){self.$results.scrollTop(0)}else if(nextBottom>currentOffset){self.$results.scrollTop(nextOffset)}});container.on('results:focus',function(params){params.element.addClass('select2-results__option--highlighted')});container.on('results:message',function(params){self.displayMessage(params)});if($.fn.mousewheel){this.$results.on('mousewheel',function(e){var top=self.$results.scrollTop();var bottom=self.$results.get(0).scrollHeight-top+e.deltaY;var isAtTop=e.deltaY>0&&top-e.deltaY<=0;var isAtBottom=e.deltaY<0&&bottom<=self.$results.height();if(isAtTop){self.$results.scrollTop(0);e.preventDefault();e.stopPropagation()}else if(isAtBottom){self.$results.scrollTop(self.$results.get(0).scrollHeight-self.$results.height());e.preventDefault();e.stopPropagation()}})}this.$results.on('mouseup','.select2-results__option[aria-selected]',function(evt){var $this=$(this);var data=Utils.GetData(this,'data');if($this.attr('aria-selected')==='true'){if(self.options.get('multiple')){self.trigger('unselect',{originalEvent:evt,data:data})}else{self.trigger('close',{})}return}self.trigger('select',{originalEvent:evt,data:data})});this.$results.on('mouseenter','.select2-results__option[aria-selected]',function(evt){var data=Utils.GetData(this,'data');self.getHighlightedResults().removeClass('select2-results__option--highlighted');self.trigger('results:focus',{data:data,element:$(this)})})};Results.prototype.getHighlightedResults=function(){var $highlighted=this.$results.find('.select2-results__option--highlighted');return $highlighted};Results.prototype.destroy=function(){this.$results.remove()};Results.prototype.ensureHighlightVisible=function(){var $highlighted=this.getHighlightedResults();if($highlighted.length===0){return}var $options=this.$results.find('[aria-selected]');var currentIndex=$options.index($highlighted);var currentOffset=this.$results.offset().top;var nextTop=$highlighted.offset().top;var nextOffset=this.$results.scrollTop()+(nextTop-currentOffset);var offsetDelta=nextTop-currentOffset;nextOffset-=$highlighted.outerHeight(false)*2;if(currentIndex<=2){this.$results.scrollTop(0)}else if(offsetDelta>this.$results.outerHeight()||offsetDelta<0){this.$results.scrollTop(nextOffset)}};Results.prototype.template=function(result,container){var template=this.options.get('templateResult');var escapeMarkup=this.options.get('escapeMarkup');var content=template(result,container);if(content==null){container.style.display='none'}else if(typeof content==='string'){container.innerHTML=escapeMarkup(content)}else{$(container).append(content)}};return Results});S2.define('select2/keys',[],function(){var KEYS={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return KEYS});S2.define('select2/selection/base',['jquery','../utils','../keys'],function($,Utils,KEYS){function BaseSelection($element,options){this.$element=$element;this.options=options;BaseSelection.__super__.constructor.call(this)}Utils.Extend(BaseSelection,Utils.Observable);BaseSelection.prototype.render=function(){var $selection=$('');this._tabindex=0;if(Utils.GetData(this.$element[0],'old-tabindex')!=null){this._tabindex=Utils.GetData(this.$element[0],'old-tabindex')}else if(this.$element.attr('tabindex')!=null){this._tabindex=this.$element.attr('tabindex')}$selection.attr('title',this.$element.attr('title'));$selection.attr('tabindex',this._tabindex);$selection.attr('aria-disabled','false');this.$selection=$selection;return $selection};BaseSelection.prototype.bind=function(container,$container){var self=this;var resultsId=container.id+'-results';this.container=container;this.$selection.on('focus',function(evt){self.trigger('focus',evt)});this.$selection.on('blur',function(evt){self._handleBlur(evt)});this.$selection.on('keydown',function(evt){self.trigger('keypress',evt);if(evt.which===KEYS.SPACE){evt.preventDefault()}});container.on('results:focus',function(params){self.$selection.attr('aria-activedescendant',params.data._resultId)});container.on('selection:update',function(params){self.update(params.data)});container.on('open',function(){self.$selection.attr('aria-expanded','true');self.$selection.attr('aria-owns',resultsId);self._attachCloseHandler(container)});container.on('close',function(){self.$selection.attr('aria-expanded','false');self.$selection.removeAttr('aria-activedescendant');self.$selection.removeAttr('aria-owns');self.$selection.trigger('focus');self._detachCloseHandler(container)});container.on('enable',function(){self.$selection.attr('tabindex',self._tabindex);self.$selection.attr('aria-disabled','false')});container.on('disable',function(){self.$selection.attr('tabindex','-1');self.$selection.attr('aria-disabled','true')})};BaseSelection.prototype._handleBlur=function(evt){var self=this;window.setTimeout(function(){if((document.activeElement==self.$selection[0])||($.contains(self.$selection[0],document.activeElement))){return}self.trigger('blur',evt)},1)};BaseSelection.prototype._attachCloseHandler=function(container){$(document.body).on('mousedown.select2.'+container.id,function(e){var $target=$(e.target);var $select=$target.closest('.select2');var $all=$('.select2.select2-container--open');$all.each(function(){if(this==$select[0]){return}var $element=Utils.GetData(this,'element');$element.select2('close')})})};BaseSelection.prototype._detachCloseHandler=function(container){$(document.body).off('mousedown.select2.'+container.id)};BaseSelection.prototype.position=function($selection,$container){var $selectionContainer=$container.find('.selection');$selectionContainer.append($selection)};BaseSelection.prototype.destroy=function(){this._detachCloseHandler(this.container)};BaseSelection.prototype.update=function(data){throw new Error('The `update` method must be defined in child classes.');};BaseSelection.prototype.isEnabled=function(){return!this.isDisabled()};BaseSelection.prototype.isDisabled=function(){return this.options.get('disabled')};return BaseSelection});S2.define('select2/selection/single',['jquery','./base','../utils','../keys'],function($,BaseSelection,Utils,KEYS){function SingleSelection(){SingleSelection.__super__.constructor.apply(this,arguments)}Utils.Extend(SingleSelection,BaseSelection);SingleSelection.prototype.render=function(){var $selection=SingleSelection.__super__.render.call(this);$selection.addClass('select2-selection--single');$selection.html(''+''+''+'');return $selection};SingleSelection.prototype.bind=function(container,$container){var self=this;SingleSelection.__super__.bind.apply(this,arguments);var id=container.id+'-container';this.$selection.find('.select2-selection__rendered').attr('id',id).attr('role','textbox').attr('aria-readonly','true');this.$selection.attr('aria-labelledby',id);this.$selection.on('mousedown',function(evt){if(evt.which!==1){return}self.trigger('toggle',{originalEvent:evt})});this.$selection.on('focus',function(evt){});this.$selection.on('blur',function(evt){});container.on('focus',function(evt){if(!container.isOpen()){self.$selection.trigger('focus')}})};SingleSelection.prototype.clear=function(){var $rendered=this.$selection.find('.select2-selection__rendered');$rendered.empty();$rendered.removeAttr('title')};SingleSelection.prototype.display=function(data,container){var template=this.options.get('templateSelection');var escapeMarkup=this.options.get('escapeMarkup');return escapeMarkup(template(data,container))};SingleSelection.prototype.selectionContainer=function(){return $('')};SingleSelection.prototype.update=function(data){if(data.length===0){this.clear();return}var selection=data[0];var $rendered=this.$selection.find('.select2-selection__rendered');var formatted=this.display(selection,$rendered);$rendered.empty().append(formatted);var title=selection.title||selection.text;if(title){$rendered.attr('title',title)}else{$rendered.removeAttr('title')}};return SingleSelection});S2.define('select2/selection/multiple',['jquery','./base','../utils'],function($,BaseSelection,Utils){function MultipleSelection($element,options){MultipleSelection.__super__.constructor.apply(this,arguments)}Utils.Extend(MultipleSelection,BaseSelection);MultipleSelection.prototype.render=function(){var $selection=MultipleSelection.__super__.render.call(this);$selection.addClass('select2-selection--multiple');$selection.html('
            ');return $selection};MultipleSelection.prototype.bind=function(container,$container){var self=this;MultipleSelection.__super__.bind.apply(this,arguments);this.$selection.on('click',function(evt){self.trigger('toggle',{originalEvent:evt})});this.$selection.on('click','.select2-selection__choice__remove',function(evt){if(self.isDisabled()){return}var $remove=$(this);var $selection=$remove.parent();var data=Utils.GetData($selection[0],'data');self.trigger('unselect',{originalEvent:evt,data:data})})};MultipleSelection.prototype.clear=function(){var $rendered=this.$selection.find('.select2-selection__rendered');$rendered.empty();$rendered.removeAttr('title')};MultipleSelection.prototype.display=function(data,container){var template=this.options.get('templateSelection');var escapeMarkup=this.options.get('escapeMarkup');return escapeMarkup(template(data,container))};MultipleSelection.prototype.selectionContainer=function(){var $container=$('
          • '+''+'×'+''+'
          • ');return $container};MultipleSelection.prototype.update=function(data){this.clear();if(data.length===0){return}var $selections=[];for(var d=0;d1;if(multipleSelections||singlePlaceholder){return decorated.call(this,data)}this.clear();var $placeholder=this.createPlaceholder(this.placeholder);this.$selection.find('.select2-selection__rendered').append($placeholder)};return Placeholder});S2.define('select2/selection/allowClear',['jquery','../keys','../utils'],function($,KEYS,Utils){function AllowClear(){}AllowClear.prototype.bind=function(decorated,container,$container){var self=this;decorated.call(this,container,$container);if(this.placeholder==null){if(this.options.get('debug')&&window.console&&console.error){console.error('Select2: The `allowClear` option should be used in combination '+'with the `placeholder` option.')}}this.$selection.on('mousedown','.select2-selection__clear',function(evt){self._handleClear(evt)});container.on('keypress',function(evt){self._handleKeyboardClear(evt,container)})};AllowClear.prototype._handleClear=function(_,evt){if(this.isDisabled()){return}var $clear=this.$selection.find('.select2-selection__clear');if($clear.length===0){return}evt.stopPropagation();var data=Utils.GetData($clear[0],'data');var previousVal=this.$element.val();this.$element.val(this.placeholder.id);var unselectData={data:data};this.trigger('clear',unselectData);if(unselectData.prevented){this.$element.val(previousVal);return}for(var d=0;d0||data.length===0){return}var removeAll=this.options.get('translations').get('removeAllItems');var $remove=$(''+'×'+'');Utils.StoreData($remove[0],'data',data);this.$selection.find('.select2-selection__rendered').prepend($remove)};return AllowClear});S2.define('select2/selection/search',['jquery','../utils','../keys'],function($,Utils,KEYS){function Search(decorated,$element,options){decorated.call(this,$element,options)}Search.prototype.render=function(decorated){var $search=$('');this.$searchContainer=$search;this.$search=$search.find('input');var $rendered=decorated.call(this);this._transferTabIndex();return $rendered};Search.prototype.bind=function(decorated,container,$container){var self=this;var resultsId=container.id+'-results';decorated.call(this,container,$container);container.on('open',function(){self.$search.attr('aria-controls',resultsId);self.$search.trigger('focus')});container.on('close',function(){self.$search.val('');self.$search.removeAttr('aria-controls');self.$search.removeAttr('aria-activedescendant');self.$search.trigger('focus')});container.on('enable',function(){self.$search.prop('disabled',false);self._transferTabIndex()});container.on('disable',function(){self.$search.prop('disabled',true)});container.on('focus',function(evt){self.$search.trigger('focus')});container.on('results:focus',function(params){if(params.data._resultId){self.$search.attr('aria-activedescendant',params.data._resultId)}else{self.$search.removeAttr('aria-activedescendant')}});this.$selection.on('focusin','.select2-search--inline',function(evt){self.trigger('focus',evt)});this.$selection.on('focusout','.select2-search--inline',function(evt){self._handleBlur(evt)});this.$selection.on('keydown','.select2-search--inline',function(evt){evt.stopPropagation();self.trigger('keypress',evt);self._keyUpPrevented=evt.isDefaultPrevented();var key=evt.which;if(key===KEYS.BACKSPACE&&self.$search.val()===''){var $previousChoice=self.$searchContainer.prev('.select2-selection__choice');if($previousChoice.length>0){var item=Utils.GetData($previousChoice[0],'data');self.searchRemoveChoice(item);evt.preventDefault()}}});this.$selection.on('click','.select2-search--inline',function(evt){if(self.$search.val()){evt.stopPropagation()}});var msie=document.documentMode;var disableInputEvents=msie&&msie<=11;this.$selection.on('input.searchcheck','.select2-search--inline',function(evt){if(disableInputEvents){self.$selection.off('input.search input.searchcheck');return}self.$selection.off('keyup.search')});this.$selection.on('keyup.search input.search','.select2-search--inline',function(evt){if(disableInputEvents&&evt.type==='input'){self.$selection.off('input.search input.searchcheck');return}var key=evt.which;if(key==KEYS.SHIFT||key==KEYS.CTRL||key==KEYS.ALT){return}if(key==KEYS.TAB){return}self.handleSearch(evt)})};Search.prototype._transferTabIndex=function(decorated){this.$search.attr('tabindex',this.$selection.attr('tabindex'));this.$selection.attr('tabindex','-1')};Search.prototype.createPlaceholder=function(decorated,placeholder){this.$search.attr('placeholder',placeholder.text)};Search.prototype.update=function(decorated,data){var searchHadFocus=this.$search[0]==document.activeElement;this.$search.attr('placeholder','');decorated.call(this,data);this.$selection.find('.select2-selection__rendered').append(this.$searchContainer);this.resizeSearch();if(searchHadFocus){this.$search.trigger('focus')}};Search.prototype.handleSearch=function(){this.resizeSearch();if(!this._keyUpPrevented){var input=this.$search.val();this.trigger('query',{term:input})}this._keyUpPrevented=false};Search.prototype.searchRemoveChoice=function(decorated,item){this.trigger('unselect',{data:item});this.$search.val(item.text);this.handleSearch()};Search.prototype.resizeSearch=function(){this.$search.css('width','25px');var width='';if(this.$search.attr('placeholder')!==''){width=this.$selection.find('.select2-selection__rendered').width()}else{var minimumWidth=this.$search.val().length+1;width=(minimumWidth*0.75)+'em'}this.$search.css('width',width)};return Search});S2.define('select2/selection/eventRelay',['jquery'],function($){function EventRelay(){}EventRelay.prototype.bind=function(decorated,container,$container){var self=this;var relayEvents=['open','opening','close','closing','select','selecting','unselect','unselecting','clear','clearing'];var preventableEvents=['opening','closing','selecting','unselecting','clearing'];decorated.call(this,container,$container);container.on('*',function(name,params){if($.inArray(name,relayEvents)===-1){return}params=params||{};var evt=$.Event('select2:'+name,{params:params});self.$element.trigger(evt);if($.inArray(name,preventableEvents)===-1){return}params.prevented=evt.isDefaultPrevented()})};return EventRelay});S2.define('select2/translation',['jquery','require'],function($,require){function Translation(dict){this.dict=dict||{}}Translation.prototype.all=function(){return this.dict};Translation.prototype.get=function(key){return this.dict[key]};Translation.prototype.extend=function(translation){this.dict=$.extend({},translation.all(),this.dict)};Translation._cache={};Translation.loadPath=function(path){if(!(path in Translation._cache)){var translations=require(path);Translation._cache[path]=translations}return new Translation(Translation._cache[path])};return Translation});S2.define('select2/diacritics',[],function(){var diacritics={'\u24B6':'A','\uFF21':'A','\u00C0':'A','\u00C1':'A','\u00C2':'A','\u1EA6':'A','\u1EA4':'A','\u1EAA':'A','\u1EA8':'A','\u00C3':'A','\u0100':'A','\u0102':'A','\u1EB0':'A','\u1EAE':'A','\u1EB4':'A','\u1EB2':'A','\u0226':'A','\u01E0':'A','\u00C4':'A','\u01DE':'A','\u1EA2':'A','\u00C5':'A','\u01FA':'A','\u01CD':'A','\u0200':'A','\u0202':'A','\u1EA0':'A','\u1EAC':'A','\u1EB6':'A','\u1E00':'A','\u0104':'A','\u023A':'A','\u2C6F':'A','\uA732':'AA','\u00C6':'AE','\u01FC':'AE','\u01E2':'AE','\uA734':'AO','\uA736':'AU','\uA738':'AV','\uA73A':'AV','\uA73C':'AY','\u24B7':'B','\uFF22':'B','\u1E02':'B','\u1E04':'B','\u1E06':'B','\u0243':'B','\u0182':'B','\u0181':'B','\u24B8':'C','\uFF23':'C','\u0106':'C','\u0108':'C','\u010A':'C','\u010C':'C','\u00C7':'C','\u1E08':'C','\u0187':'C','\u023B':'C','\uA73E':'C','\u24B9':'D','\uFF24':'D','\u1E0A':'D','\u010E':'D','\u1E0C':'D','\u1E10':'D','\u1E12':'D','\u1E0E':'D','\u0110':'D','\u018B':'D','\u018A':'D','\u0189':'D','\uA779':'D','\u01F1':'DZ','\u01C4':'DZ','\u01F2':'Dz','\u01C5':'Dz','\u24BA':'E','\uFF25':'E','\u00C8':'E','\u00C9':'E','\u00CA':'E','\u1EC0':'E','\u1EBE':'E','\u1EC4':'E','\u1EC2':'E','\u1EBC':'E','\u0112':'E','\u1E14':'E','\u1E16':'E','\u0114':'E','\u0116':'E','\u00CB':'E','\u1EBA':'E','\u011A':'E','\u0204':'E','\u0206':'E','\u1EB8':'E','\u1EC6':'E','\u0228':'E','\u1E1C':'E','\u0118':'E','\u1E18':'E','\u1E1A':'E','\u0190':'E','\u018E':'E','\u24BB':'F','\uFF26':'F','\u1E1E':'F','\u0191':'F','\uA77B':'F','\u24BC':'G','\uFF27':'G','\u01F4':'G','\u011C':'G','\u1E20':'G','\u011E':'G','\u0120':'G','\u01E6':'G','\u0122':'G','\u01E4':'G','\u0193':'G','\uA7A0':'G','\uA77D':'G','\uA77E':'G','\u24BD':'H','\uFF28':'H','\u0124':'H','\u1E22':'H','\u1E26':'H','\u021E':'H','\u1E24':'H','\u1E28':'H','\u1E2A':'H','\u0126':'H','\u2C67':'H','\u2C75':'H','\uA78D':'H','\u24BE':'I','\uFF29':'I','\u00CC':'I','\u00CD':'I','\u00CE':'I','\u0128':'I','\u012A':'I','\u012C':'I','\u0130':'I','\u00CF':'I','\u1E2E':'I','\u1EC8':'I','\u01CF':'I','\u0208':'I','\u020A':'I','\u1ECA':'I','\u012E':'I','\u1E2C':'I','\u0197':'I','\u24BF':'J','\uFF2A':'J','\u0134':'J','\u0248':'J','\u24C0':'K','\uFF2B':'K','\u1E30':'K','\u01E8':'K','\u1E32':'K','\u0136':'K','\u1E34':'K','\u0198':'K','\u2C69':'K','\uA740':'K','\uA742':'K','\uA744':'K','\uA7A2':'K','\u24C1':'L','\uFF2C':'L','\u013F':'L','\u0139':'L','\u013D':'L','\u1E36':'L','\u1E38':'L','\u013B':'L','\u1E3C':'L','\u1E3A':'L','\u0141':'L','\u023D':'L','\u2C62':'L','\u2C60':'L','\uA748':'L','\uA746':'L','\uA780':'L','\u01C7':'LJ','\u01C8':'Lj','\u24C2':'M','\uFF2D':'M','\u1E3E':'M','\u1E40':'M','\u1E42':'M','\u2C6E':'M','\u019C':'M','\u24C3':'N','\uFF2E':'N','\u01F8':'N','\u0143':'N','\u00D1':'N','\u1E44':'N','\u0147':'N','\u1E46':'N','\u0145':'N','\u1E4A':'N','\u1E48':'N','\u0220':'N','\u019D':'N','\uA790':'N','\uA7A4':'N','\u01CA':'NJ','\u01CB':'Nj','\u24C4':'O','\uFF2F':'O','\u00D2':'O','\u00D3':'O','\u00D4':'O','\u1ED2':'O','\u1ED0':'O','\u1ED6':'O','\u1ED4':'O','\u00D5':'O','\u1E4C':'O','\u022C':'O','\u1E4E':'O','\u014C':'O','\u1E50':'O','\u1E52':'O','\u014E':'O','\u022E':'O','\u0230':'O','\u00D6':'O','\u022A':'O','\u1ECE':'O','\u0150':'O','\u01D1':'O','\u020C':'O','\u020E':'O','\u01A0':'O','\u1EDC':'O','\u1EDA':'O','\u1EE0':'O','\u1EDE':'O','\u1EE2':'O','\u1ECC':'O','\u1ED8':'O','\u01EA':'O','\u01EC':'O','\u00D8':'O','\u01FE':'O','\u0186':'O','\u019F':'O','\uA74A':'O','\uA74C':'O','\u0152':'OE','\u01A2':'OI','\uA74E':'OO','\u0222':'OU','\u24C5':'P','\uFF30':'P','\u1E54':'P','\u1E56':'P','\u01A4':'P','\u2C63':'P','\uA750':'P','\uA752':'P','\uA754':'P','\u24C6':'Q','\uFF31':'Q','\uA756':'Q','\uA758':'Q','\u024A':'Q','\u24C7':'R','\uFF32':'R','\u0154':'R','\u1E58':'R','\u0158':'R','\u0210':'R','\u0212':'R','\u1E5A':'R','\u1E5C':'R','\u0156':'R','\u1E5E':'R','\u024C':'R','\u2C64':'R','\uA75A':'R','\uA7A6':'R','\uA782':'R','\u24C8':'S','\uFF33':'S','\u1E9E':'S','\u015A':'S','\u1E64':'S','\u015C':'S','\u1E60':'S','\u0160':'S','\u1E66':'S','\u1E62':'S','\u1E68':'S','\u0218':'S','\u015E':'S','\u2C7E':'S','\uA7A8':'S','\uA784':'S','\u24C9':'T','\uFF34':'T','\u1E6A':'T','\u0164':'T','\u1E6C':'T','\u021A':'T','\u0162':'T','\u1E70':'T','\u1E6E':'T','\u0166':'T','\u01AC':'T','\u01AE':'T','\u023E':'T','\uA786':'T','\uA728':'TZ','\u24CA':'U','\uFF35':'U','\u00D9':'U','\u00DA':'U','\u00DB':'U','\u0168':'U','\u1E78':'U','\u016A':'U','\u1E7A':'U','\u016C':'U','\u00DC':'U','\u01DB':'U','\u01D7':'U','\u01D5':'U','\u01D9':'U','\u1EE6':'U','\u016E':'U','\u0170':'U','\u01D3':'U','\u0214':'U','\u0216':'U','\u01AF':'U','\u1EEA':'U','\u1EE8':'U','\u1EEE':'U','\u1EEC':'U','\u1EF0':'U','\u1EE4':'U','\u1E72':'U','\u0172':'U','\u1E76':'U','\u1E74':'U','\u0244':'U','\u24CB':'V','\uFF36':'V','\u1E7C':'V','\u1E7E':'V','\u01B2':'V','\uA75E':'V','\u0245':'V','\uA760':'VY','\u24CC':'W','\uFF37':'W','\u1E80':'W','\u1E82':'W','\u0174':'W','\u1E86':'W','\u1E84':'W','\u1E88':'W','\u2C72':'W','\u24CD':'X','\uFF38':'X','\u1E8A':'X','\u1E8C':'X','\u24CE':'Y','\uFF39':'Y','\u1EF2':'Y','\u00DD':'Y','\u0176':'Y','\u1EF8':'Y','\u0232':'Y','\u1E8E':'Y','\u0178':'Y','\u1EF6':'Y','\u1EF4':'Y','\u01B3':'Y','\u024E':'Y','\u1EFE':'Y','\u24CF':'Z','\uFF3A':'Z','\u0179':'Z','\u1E90':'Z','\u017B':'Z','\u017D':'Z','\u1E92':'Z','\u1E94':'Z','\u01B5':'Z','\u0224':'Z','\u2C7F':'Z','\u2C6B':'Z','\uA762':'Z','\u24D0':'a','\uFF41':'a','\u1E9A':'a','\u00E0':'a','\u00E1':'a','\u00E2':'a','\u1EA7':'a','\u1EA5':'a','\u1EAB':'a','\u1EA9':'a','\u00E3':'a','\u0101':'a','\u0103':'a','\u1EB1':'a','\u1EAF':'a','\u1EB5':'a','\u1EB3':'a','\u0227':'a','\u01E1':'a','\u00E4':'a','\u01DF':'a','\u1EA3':'a','\u00E5':'a','\u01FB':'a','\u01CE':'a','\u0201':'a','\u0203':'a','\u1EA1':'a','\u1EAD':'a','\u1EB7':'a','\u1E01':'a','\u0105':'a','\u2C65':'a','\u0250':'a','\uA733':'aa','\u00E6':'ae','\u01FD':'ae','\u01E3':'ae','\uA735':'ao','\uA737':'au','\uA739':'av','\uA73B':'av','\uA73D':'ay','\u24D1':'b','\uFF42':'b','\u1E03':'b','\u1E05':'b','\u1E07':'b','\u0180':'b','\u0183':'b','\u0253':'b','\u24D2':'c','\uFF43':'c','\u0107':'c','\u0109':'c','\u010B':'c','\u010D':'c','\u00E7':'c','\u1E09':'c','\u0188':'c','\u023C':'c','\uA73F':'c','\u2184':'c','\u24D3':'d','\uFF44':'d','\u1E0B':'d','\u010F':'d','\u1E0D':'d','\u1E11':'d','\u1E13':'d','\u1E0F':'d','\u0111':'d','\u018C':'d','\u0256':'d','\u0257':'d','\uA77A':'d','\u01F3':'dz','\u01C6':'dz','\u24D4':'e','\uFF45':'e','\u00E8':'e','\u00E9':'e','\u00EA':'e','\u1EC1':'e','\u1EBF':'e','\u1EC5':'e','\u1EC3':'e','\u1EBD':'e','\u0113':'e','\u1E15':'e','\u1E17':'e','\u0115':'e','\u0117':'e','\u00EB':'e','\u1EBB':'e','\u011B':'e','\u0205':'e','\u0207':'e','\u1EB9':'e','\u1EC7':'e','\u0229':'e','\u1E1D':'e','\u0119':'e','\u1E19':'e','\u1E1B':'e','\u0247':'e','\u025B':'e','\u01DD':'e','\u24D5':'f','\uFF46':'f','\u1E1F':'f','\u0192':'f','\uA77C':'f','\u24D6':'g','\uFF47':'g','\u01F5':'g','\u011D':'g','\u1E21':'g','\u011F':'g','\u0121':'g','\u01E7':'g','\u0123':'g','\u01E5':'g','\u0260':'g','\uA7A1':'g','\u1D79':'g','\uA77F':'g','\u24D7':'h','\uFF48':'h','\u0125':'h','\u1E23':'h','\u1E27':'h','\u021F':'h','\u1E25':'h','\u1E29':'h','\u1E2B':'h','\u1E96':'h','\u0127':'h','\u2C68':'h','\u2C76':'h','\u0265':'h','\u0195':'hv','\u24D8':'i','\uFF49':'i','\u00EC':'i','\u00ED':'i','\u00EE':'i','\u0129':'i','\u012B':'i','\u012D':'i','\u00EF':'i','\u1E2F':'i','\u1EC9':'i','\u01D0':'i','\u0209':'i','\u020B':'i','\u1ECB':'i','\u012F':'i','\u1E2D':'i','\u0268':'i','\u0131':'i','\u24D9':'j','\uFF4A':'j','\u0135':'j','\u01F0':'j','\u0249':'j','\u24DA':'k','\uFF4B':'k','\u1E31':'k','\u01E9':'k','\u1E33':'k','\u0137':'k','\u1E35':'k','\u0199':'k','\u2C6A':'k','\uA741':'k','\uA743':'k','\uA745':'k','\uA7A3':'k','\u24DB':'l','\uFF4C':'l','\u0140':'l','\u013A':'l','\u013E':'l','\u1E37':'l','\u1E39':'l','\u013C':'l','\u1E3D':'l','\u1E3B':'l','\u017F':'l','\u0142':'l','\u019A':'l','\u026B':'l','\u2C61':'l','\uA749':'l','\uA781':'l','\uA747':'l','\u01C9':'lj','\u24DC':'m','\uFF4D':'m','\u1E3F':'m','\u1E41':'m','\u1E43':'m','\u0271':'m','\u026F':'m','\u24DD':'n','\uFF4E':'n','\u01F9':'n','\u0144':'n','\u00F1':'n','\u1E45':'n','\u0148':'n','\u1E47':'n','\u0146':'n','\u1E4B':'n','\u1E49':'n','\u019E':'n','\u0272':'n','\u0149':'n','\uA791':'n','\uA7A5':'n','\u01CC':'nj','\u24DE':'o','\uFF4F':'o','\u00F2':'o','\u00F3':'o','\u00F4':'o','\u1ED3':'o','\u1ED1':'o','\u1ED7':'o','\u1ED5':'o','\u00F5':'o','\u1E4D':'o','\u022D':'o','\u1E4F':'o','\u014D':'o','\u1E51':'o','\u1E53':'o','\u014F':'o','\u022F':'o','\u0231':'o','\u00F6':'o','\u022B':'o','\u1ECF':'o','\u0151':'o','\u01D2':'o','\u020D':'o','\u020F':'o','\u01A1':'o','\u1EDD':'o','\u1EDB':'o','\u1EE1':'o','\u1EDF':'o','\u1EE3':'o','\u1ECD':'o','\u1ED9':'o','\u01EB':'o','\u01ED':'o','\u00F8':'o','\u01FF':'o','\u0254':'o','\uA74B':'o','\uA74D':'o','\u0275':'o','\u0153':'oe','\u01A3':'oi','\u0223':'ou','\uA74F':'oo','\u24DF':'p','\uFF50':'p','\u1E55':'p','\u1E57':'p','\u01A5':'p','\u1D7D':'p','\uA751':'p','\uA753':'p','\uA755':'p','\u24E0':'q','\uFF51':'q','\u024B':'q','\uA757':'q','\uA759':'q','\u24E1':'r','\uFF52':'r','\u0155':'r','\u1E59':'r','\u0159':'r','\u0211':'r','\u0213':'r','\u1E5B':'r','\u1E5D':'r','\u0157':'r','\u1E5F':'r','\u024D':'r','\u027D':'r','\uA75B':'r','\uA7A7':'r','\uA783':'r','\u24E2':'s','\uFF53':'s','\u00DF':'s','\u015B':'s','\u1E65':'s','\u015D':'s','\u1E61':'s','\u0161':'s','\u1E67':'s','\u1E63':'s','\u1E69':'s','\u0219':'s','\u015F':'s','\u023F':'s','\uA7A9':'s','\uA785':'s','\u1E9B':'s','\u24E3':'t','\uFF54':'t','\u1E6B':'t','\u1E97':'t','\u0165':'t','\u1E6D':'t','\u021B':'t','\u0163':'t','\u1E71':'t','\u1E6F':'t','\u0167':'t','\u01AD':'t','\u0288':'t','\u2C66':'t','\uA787':'t','\uA729':'tz','\u24E4':'u','\uFF55':'u','\u00F9':'u','\u00FA':'u','\u00FB':'u','\u0169':'u','\u1E79':'u','\u016B':'u','\u1E7B':'u','\u016D':'u','\u00FC':'u','\u01DC':'u','\u01D8':'u','\u01D6':'u','\u01DA':'u','\u1EE7':'u','\u016F':'u','\u0171':'u','\u01D4':'u','\u0215':'u','\u0217':'u','\u01B0':'u','\u1EEB':'u','\u1EE9':'u','\u1EEF':'u','\u1EED':'u','\u1EF1':'u','\u1EE5':'u','\u1E73':'u','\u0173':'u','\u1E77':'u','\u1E75':'u','\u0289':'u','\u24E5':'v','\uFF56':'v','\u1E7D':'v','\u1E7F':'v','\u028B':'v','\uA75F':'v','\u028C':'v','\uA761':'vy','\u24E6':'w','\uFF57':'w','\u1E81':'w','\u1E83':'w','\u0175':'w','\u1E87':'w','\u1E85':'w','\u1E98':'w','\u1E89':'w','\u2C73':'w','\u24E7':'x','\uFF58':'x','\u1E8B':'x','\u1E8D':'x','\u24E8':'y','\uFF59':'y','\u1EF3':'y','\u00FD':'y','\u0177':'y','\u1EF9':'y','\u0233':'y','\u1E8F':'y','\u00FF':'y','\u1EF7':'y','\u1E99':'y','\u1EF5':'y','\u01B4':'y','\u024F':'y','\u1EFF':'y','\u24E9':'z','\uFF5A':'z','\u017A':'z','\u1E91':'z','\u017C':'z','\u017E':'z','\u1E93':'z','\u1E95':'z','\u01B6':'z','\u0225':'z','\u0240':'z','\u2C6C':'z','\uA763':'z','\u0386':'\u0391','\u0388':'\u0395','\u0389':'\u0397','\u038A':'\u0399','\u03AA':'\u0399','\u038C':'\u039F','\u038E':'\u03A5','\u03AB':'\u03A5','\u038F':'\u03A9','\u03AC':'\u03B1','\u03AD':'\u03B5','\u03AE':'\u03B7','\u03AF':'\u03B9','\u03CA':'\u03B9','\u0390':'\u03B9','\u03CC':'\u03BF','\u03CD':'\u03C5','\u03CB':'\u03C5','\u03B0':'\u03C5','\u03CE':'\u03C9','\u03C2':'\u03C3','\u2019':'\''};return diacritics});S2.define('select2/data/base',['../utils'],function(Utils){function BaseAdapter($element,options){BaseAdapter.__super__.constructor.call(this)}Utils.Extend(BaseAdapter,Utils.Observable);BaseAdapter.prototype.current=function(callback){throw new Error('The `current` method must be defined in child classes.');};BaseAdapter.prototype.query=function(params,callback){throw new Error('The `query` method must be defined in child classes.');};BaseAdapter.prototype.bind=function(container,$container){};BaseAdapter.prototype.destroy=function(){};BaseAdapter.prototype.generateResultId=function(container,data){var id=container.id+'-result-';id+=Utils.generateChars(4);if(data.id!=null){id+='-'+data.id.toString()}else{id+='-'+Utils.generateChars(4)}return id};return BaseAdapter});S2.define('select2/data/select',['./base','../utils','jquery'],function(BaseAdapter,Utils,$){function SelectAdapter($element,options){this.$element=$element;this.options=options;SelectAdapter.__super__.constructor.call(this)}Utils.Extend(SelectAdapter,BaseAdapter);SelectAdapter.prototype.current=function(callback){var data=[];var self=this;this.$element.find(':selected').each(function(){var $option=$(this);var option=self.item($option);data.push(option)});callback(data)};SelectAdapter.prototype.select=function(data){var self=this;data.selected=true;if($(data.element).is('option')){data.element.selected=true;this.$element.trigger('input').trigger('change');return}if(this.$element.prop('multiple')){this.current(function(currentData){var val=[];data=[data];data.push.apply(data,currentData);for(var d=0;d=0){var $existingOption=$existing.filter(onlyItem(item));var existingData=this.item($existingOption);var newData=$.extend(true,{},item,existingData);var $newOption=this.option(newData);$existingOption.replaceWith($newOption);continue}var $option=this.option(item);if(item.children){var $children=this.convertToOptions(item.children);Utils.appendMany($option,$children)}$options.push($option)}return $options};return ArrayAdapter});S2.define('select2/data/ajax',['./array','../utils','jquery'],function(ArrayAdapter,Utils,$){function AjaxAdapter($element,options){this.ajaxOptions=this._applyDefaults(options.get('ajax'));if(this.ajaxOptions.processResults!=null){this.processResults=this.ajaxOptions.processResults}AjaxAdapter.__super__.constructor.call(this,$element,options)}Utils.Extend(AjaxAdapter,ArrayAdapter);AjaxAdapter.prototype._applyDefaults=function(options){var defaults={data:function(params){return $.extend({},params,{q:params.term})},transport:function(params,success,failure){var $request=$.ajax(params);$request.then(success);$request.fail(failure);return $request}};return $.extend({},defaults,options,true)};AjaxAdapter.prototype.processResults=function(results){return results};AjaxAdapter.prototype.query=function(params,callback){var matches=[];var self=this;if(this._request!=null){if($.isFunction(this._request.abort)){this._request.abort()}this._request=null}var options=$.extend({type:'GET'},this.ajaxOptions);if(typeof options.url==='function'){options.url=options.url.call(this.$element,params)}if(typeof options.data==='function'){options.data=options.data.call(this.$element,params)}function request(){var $request=options.transport(options,function(data){var results=self.processResults(data,params);if(self.options.get('debug')&&window.console&&console.error){if(!results||!results.results||!$.isArray(results.results)){console.error('Select2: The AJAX results did not return an array in the '+'`results` key of the response.')}}callback(results)},function(){if('status'in $request&&($request.status===0||$request.status==='0')){return}self.trigger('results:message',{message:'errorLoading'})});self._request=$request}if(this.ajaxOptions.delay&¶ms.term!=null){if(this._queryTimeout){window.clearTimeout(this._queryTimeout)}this._queryTimeout=window.setTimeout(request,this.ajaxOptions.delay)}else{request()}};return AjaxAdapter});S2.define('select2/data/tags',['jquery'],function($){function Tags(decorated,$element,options){var tags=options.get('tags');var createTag=options.get('createTag');if(createTag!==undefined){this.createTag=createTag}var insertTag=options.get('insertTag');if(insertTag!==undefined){this.insertTag=insertTag}decorated.call(this,$element,options);if($.isArray(tags)){for(var t=0;t0&¶ms.term.length>this.maximumInputLength){this.trigger('results:message',{message:'inputTooLong',args:{maximum:this.maximumInputLength,input:params.term,params:params}});return}decorated.call(this,params,callback)};return MaximumInputLength});S2.define('select2/data/maximumSelectionLength',[],function(){function MaximumSelectionLength(decorated,$e,options){this.maximumSelectionLength=options.get('maximumSelectionLength');decorated.call(this,$e,options)}MaximumSelectionLength.prototype.bind=function(decorated,container,$container){var self=this;decorated.call(this,container,$container);container.on('select',function(){self._checkIfMaximumSelected()})};MaximumSelectionLength.prototype.query=function(decorated,params,callback){var self=this;this._checkIfMaximumSelected(function(){decorated.call(self,params,callback)})};MaximumSelectionLength.prototype._checkIfMaximumSelected=function(_,successCallback){var self=this;this.current(function(currentData){var count=currentData!=null?currentData.length:0;if(self.maximumSelectionLength>0&&count>=self.maximumSelectionLength){self.trigger('results:message',{message:'maximumSelected',args:{maximum:self.maximumSelectionLength}});return}if(successCallback){successCallback()}})};return MaximumSelectionLength});S2.define('select2/dropdown',['jquery','./utils'],function($,Utils){function Dropdown($element,options){this.$element=$element;this.options=options;Dropdown.__super__.constructor.call(this)}Utils.Extend(Dropdown,Utils.Observable);Dropdown.prototype.render=function(){var $dropdown=$(''+''+'');$dropdown.attr('dir',this.options.get('dir'));this.$dropdown=$dropdown;return $dropdown};Dropdown.prototype.bind=function(){};Dropdown.prototype.position=function($dropdown,$container){};Dropdown.prototype.destroy=function(){this.$dropdown.remove()};return Dropdown});S2.define('select2/dropdown/search',['jquery','../utils'],function($,Utils){function Search(){}Search.prototype.render=function(decorated){var $rendered=decorated.call(this);var $search=$(''+''+'');this.$searchContainer=$search;this.$search=$search.find('input');$rendered.prepend($search);return $rendered};Search.prototype.bind=function(decorated,container,$container){var self=this;var resultsId=container.id+'-results';decorated.call(this,container,$container);this.$search.on('keydown',function(evt){self.trigger('keypress',evt);self._keyUpPrevented=evt.isDefaultPrevented()});this.$search.on('input',function(evt){$(this).off('keyup')});this.$search.on('keyup input',function(evt){self.handleSearch(evt)});container.on('open',function(){self.$search.attr('tabindex',0);self.$search.attr('aria-controls',resultsId);self.$search.trigger('focus');window.setTimeout(function(){self.$search.trigger('focus')},0)});container.on('close',function(){self.$search.attr('tabindex',-1);self.$search.removeAttr('aria-controls');self.$search.removeAttr('aria-activedescendant');self.$search.val('');self.$search.trigger('blur')});container.on('focus',function(){if(!container.isOpen()){self.$search.trigger('focus')}});container.on('results:all',function(params){if(params.query.term==null||params.query.term===''){var showSearch=self.showSearch(params);if(showSearch){self.$searchContainer.removeClass('select2-search--hide')}else{self.$searchContainer.addClass('select2-search--hide')}}});container.on('results:focus',function(params){if(params.data._resultId){self.$search.attr('aria-activedescendant',params.data._resultId)}else{self.$search.removeAttr('aria-activedescendant')}})};Search.prototype.handleSearch=function(evt){if(!this._keyUpPrevented){var input=this.$search.val();this.trigger('query',{term:input})}this._keyUpPrevented=false};Search.prototype.showSearch=function(_,params){return true};return Search});S2.define('select2/dropdown/hidePlaceholder',[],function(){function HidePlaceholder(decorated,$element,options,dataAdapter){this.placeholder=this.normalizePlaceholder(options.get('placeholder'));decorated.call(this,$element,options,dataAdapter)}HidePlaceholder.prototype.append=function(decorated,data){data.results=this.removePlaceholder(data.results);decorated.call(this,data)};HidePlaceholder.prototype.normalizePlaceholder=function(_,placeholder){if(typeof placeholder==='string'){placeholder={id:'',text:placeholder}}return placeholder};HidePlaceholder.prototype.removePlaceholder=function(_,data){var modifiedData=data.slice(0);for(var d=data.length-1;d>=0;d--){var item=data[d];if(this.placeholder.id===item.id){modifiedData.splice(d,1)}}return modifiedData};return HidePlaceholder});S2.define('select2/dropdown/infiniteScroll',['jquery'],function($){function InfiniteScroll(decorated,$element,options,dataAdapter){this.lastParams={};decorated.call(this,$element,options,dataAdapter);this.$loadingMore=this.createLoadingMore();this.loading=false}InfiniteScroll.prototype.append=function(decorated,data){this.$loadingMore.remove();this.loading=false;decorated.call(this,data);if(this.showLoadingMore(data)){this.$results.append(this.$loadingMore);this.loadMoreIfNeeded()}};InfiniteScroll.prototype.bind=function(decorated,container,$container){var self=this;decorated.call(this,container,$container);container.on('query',function(params){self.lastParams=params;self.loading=true});container.on('query:append',function(params){self.lastParams=params;self.loading=true});this.$results.on('scroll',this.loadMoreIfNeeded.bind(this))};InfiniteScroll.prototype.loadMoreIfNeeded=function(){var isLoadMoreVisible=$.contains(document.documentElement,this.$loadingMore[0]);if(this.loading||!isLoadMoreVisible){return}var currentOffset=this.$results.offset().top+this.$results.outerHeight(false);var loadingMoreOffset=this.$loadingMore.offset().top+this.$loadingMore.outerHeight(false);if(currentOffset+50>=loadingMoreOffset){this.loadMore()}};InfiniteScroll.prototype.loadMore=function(){this.loading=true;var params=$.extend({},{page:1},this.lastParams);params.page++;this.trigger('query:append',params)};InfiniteScroll.prototype.showLoadingMore=function(_,data){return data.pagination&&data.pagination.more};InfiniteScroll.prototype.createLoadingMore=function(){var $option=$('
          • ');var message=this.options.get('translations').get('loadingMore');$option.html(message(this.lastParams));return $option};return InfiniteScroll});S2.define('select2/dropdown/attachBody',['jquery','../utils'],function($,Utils){function AttachBody(decorated,$element,options){this.$dropdownParent=$(options.get('dropdownParent')||document.body);decorated.call(this,$element,options)}AttachBody.prototype.bind=function(decorated,container,$container){var self=this;decorated.call(this,container,$container);container.on('open',function(){self._showDropdown();self._attachPositioningHandler(container);self._bindContainerResultHandlers(container)});container.on('close',function(){self._hideDropdown();self._detachPositioningHandler(container)});this.$dropdownContainer.on('mousedown',function(evt){evt.stopPropagation()})};AttachBody.prototype.destroy=function(decorated){decorated.call(this);this.$dropdownContainer.remove()};AttachBody.prototype.position=function(decorated,$dropdown,$container){$dropdown.attr('class',$container.attr('class'));$dropdown.removeClass('select2');$dropdown.addClass('select2-container--open');$dropdown.css({position:'absolute',top:-999999});this.$container=$container};AttachBody.prototype.render=function(decorated){var $container=$('');var $dropdown=decorated.call(this);$container.append($dropdown);this.$dropdownContainer=$container;return $container};AttachBody.prototype._hideDropdown=function(decorated){this.$dropdownContainer.detach()};AttachBody.prototype._bindContainerResultHandlers=function(decorated,container){if(this._containerResultsHandlersBound){return}var self=this;container.on('results:all',function(){self._positionDropdown();self._resizeDropdown()});container.on('results:append',function(){self._positionDropdown();self._resizeDropdown()});container.on('results:message',function(){self._positionDropdown();self._resizeDropdown()});container.on('select',function(){self._positionDropdown();self._resizeDropdown()});container.on('unselect',function(){self._positionDropdown();self._resizeDropdown()});this._containerResultsHandlersBound=true};AttachBody.prototype._attachPositioningHandler=function(decorated,container){var self=this;var scrollEvent='scroll.select2.'+container.id;var resizeEvent='resize.select2.'+container.id;var orientationEvent='orientationchange.select2.'+container.id;var $watchers=this.$container.parents().filter(Utils.hasScroll);$watchers.each(function(){Utils.StoreData(this,'select2-scroll-position',{x:$(this).scrollLeft(),y:$(this).scrollTop()})});$watchers.on(scrollEvent,function(ev){var position=Utils.GetData(this,'select2-scroll-position');$(this).scrollTop(position.y)});$(window).on(scrollEvent+' '+resizeEvent+' '+orientationEvent,function(e){self._positionDropdown();self._resizeDropdown()})};AttachBody.prototype._detachPositioningHandler=function(decorated,container){var scrollEvent='scroll.select2.'+container.id;var resizeEvent='resize.select2.'+container.id;var orientationEvent='orientationchange.select2.'+container.id;var $watchers=this.$container.parents().filter(Utils.hasScroll);$watchers.off(scrollEvent);$(window).off(scrollEvent+' '+resizeEvent+' '+orientationEvent)};AttachBody.prototype._positionDropdown=function(){var $window=$(window);var isCurrentlyAbove=this.$dropdown.hasClass('select2-dropdown--above');var isCurrentlyBelow=this.$dropdown.hasClass('select2-dropdown--below');var newDirection=null;var offset=this.$container.offset();offset.bottom=offset.top+this.$container.outerHeight(false);var container={height:this.$container.outerHeight(false)};container.top=offset.top;container.bottom=offset.top+container.height;var dropdown={height:this.$dropdown.outerHeight(false)};var viewport={top:$window.scrollTop(),bottom:$window.scrollTop()+$window.height()};var enoughRoomAbove=viewport.top<(offset.top-dropdown.height);var enoughRoomBelow=viewport.bottom>(offset.bottom+dropdown.height);var css={left:offset.left,top:container.bottom};var $offsetParent=this.$dropdownParent;if($offsetParent.css('position')==='static'){$offsetParent=$offsetParent.offsetParent()}var parentOffset={top:0,left:0};if($.contains(document.body,$offsetParent[0])||$offsetParent[0].isConnected){parentOffset=$offsetParent.offset()}css.top-=parentOffset.top;css.left-=parentOffset.left;if(!isCurrentlyAbove&&!isCurrentlyBelow){newDirection='below'}if(!enoughRoomBelow&&enoughRoomAbove&&!isCurrentlyAbove){newDirection='above'}else if(!enoughRoomAbove&&enoughRoomBelow&&isCurrentlyAbove){newDirection='below'}if(newDirection=='above'||(isCurrentlyAbove&&newDirection!=='below')){css.top=container.top-parentOffset.top-dropdown.height}if(newDirection!=null){this.$dropdown.removeClass('select2-dropdown--below select2-dropdown--above').addClass('select2-dropdown--'+newDirection);this.$container.removeClass('select2-container--below select2-container--above').addClass('select2-container--'+newDirection)}this.$dropdownContainer.css(css)};AttachBody.prototype._resizeDropdown=function(){var css={width:this.$container.outerWidth(false)+'px'};if(this.options.get('dropdownAutoWidth')){css.minWidth=css.width;css.position='relative';css.width='auto'}this.$dropdown.css(css)};AttachBody.prototype._showDropdown=function(decorated){this.$dropdownContainer.appendTo(this.$dropdownParent);this._positionDropdown();this._resizeDropdown()};return AttachBody});S2.define('select2/dropdown/minimumResultsForSearch',[],function(){function countResults(data){var count=0;for(var d=0;d0){options.dataAdapter=Utils.Decorate(options.dataAdapter,MinimumInputLength)}if(options.maximumInputLength>0){options.dataAdapter=Utils.Decorate(options.dataAdapter,MaximumInputLength)}if(options.maximumSelectionLength>0){options.dataAdapter=Utils.Decorate(options.dataAdapter,MaximumSelectionLength)}if(options.tags){options.dataAdapter=Utils.Decorate(options.dataAdapter,Tags)}if(options.tokenSeparators!=null||options.tokenizer!=null){options.dataAdapter=Utils.Decorate(options.dataAdapter,Tokenizer)}if(options.query!=null){var Query=require(options.amdBase+'compat/query');options.dataAdapter=Utils.Decorate(options.dataAdapter,Query)}if(options.initSelection!=null){var InitSelection=require(options.amdBase+'compat/initSelection');options.dataAdapter=Utils.Decorate(options.dataAdapter,InitSelection)}}if(options.resultsAdapter==null){options.resultsAdapter=ResultsList;if(options.ajax!=null){options.resultsAdapter=Utils.Decorate(options.resultsAdapter,InfiniteScroll)}if(options.placeholder!=null){options.resultsAdapter=Utils.Decorate(options.resultsAdapter,HidePlaceholder)}if(options.selectOnClose){options.resultsAdapter=Utils.Decorate(options.resultsAdapter,SelectOnClose)}}if(options.dropdownAdapter==null){if(options.multiple){options.dropdownAdapter=Dropdown}else{var SearchableDropdown=Utils.Decorate(Dropdown,DropdownSearch);options.dropdownAdapter=SearchableDropdown}if(options.minimumResultsForSearch!==0){options.dropdownAdapter=Utils.Decorate(options.dropdownAdapter,MinimumResultsForSearch)}if(options.closeOnSelect){options.dropdownAdapter=Utils.Decorate(options.dropdownAdapter,CloseOnSelect)}if(options.dropdownCssClass!=null||options.dropdownCss!=null||options.adaptDropdownCssClass!=null){var DropdownCSS=require(options.amdBase+'compat/dropdownCss');options.dropdownAdapter=Utils.Decorate(options.dropdownAdapter,DropdownCSS)}options.dropdownAdapter=Utils.Decorate(options.dropdownAdapter,AttachBody)}if(options.selectionAdapter==null){if(options.multiple){options.selectionAdapter=MultipleSelection}else{options.selectionAdapter=SingleSelection}if(options.placeholder!=null){options.selectionAdapter=Utils.Decorate(options.selectionAdapter,Placeholder)}if(options.allowClear){options.selectionAdapter=Utils.Decorate(options.selectionAdapter,AllowClear)}if(options.multiple){options.selectionAdapter=Utils.Decorate(options.selectionAdapter,SelectionSearch)}if(options.containerCssClass!=null||options.containerCss!=null||options.adaptContainerCssClass!=null){var ContainerCSS=require(options.amdBase+'compat/containerCss');options.selectionAdapter=Utils.Decorate(options.selectionAdapter,ContainerCSS)}options.selectionAdapter=Utils.Decorate(options.selectionAdapter,EventRelay)}options.language=this._resolveLanguage(options.language);options.language.push('en');var uniqueLanguages=[];for(var l=0;l0){var match=$.extend(true,{},data);for(var c=data.children.length-1;c>=0;c--){var child=data.children[c];var matches=matcher(params,child);if(matches==null){match.children.splice(c,1)}}if(match.children.length>0){return match}return matcher(params,match)}var original=stripDiacritics(data.text).toUpperCase();var term=stripDiacritics(params.term).toUpperCase();if(original.indexOf(term)>-1){return data}return null}this.defaults={amdBase:'./',amdLanguageBase:'./i18n/',closeOnSelect:true,debug:false,dropdownAutoWidth:false,escapeMarkup:Utils.escapeMarkup,language:{},matcher:matcher,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:false,scrollAfterSelect:false,sorter:function(data){return data},templateResult:function(result){return result.text},templateSelection:function(selection){return selection.text},theme:'default',width:'resolve'}};Defaults.prototype.applyFromElement=function(options,$element){var optionLanguage=options.language;var defaultLanguage=this.defaults.language;var elementLanguage=$element.prop('lang');var parentLanguage=$element.closest('[lang]').prop('lang');var languages=Array.prototype.concat.call(this._resolveLanguage(elementLanguage),this._resolveLanguage(optionLanguage),this._resolveLanguage(defaultLanguage),this._resolveLanguage(parentLanguage));options.language=languages;return options};Defaults.prototype._resolveLanguage=function(language){if(!language){return[]}if($.isEmptyObject(language)){return[]}if($.isPlainObject(language)){return[language]}var languages;if(!$.isArray(language)){languages=[language]}else{languages=language}var resolvedLanguages=[];for(var l=0;l0){var languageParts=languages[l].split('-');var baseLanguage=languageParts[0];resolvedLanguages.push(baseLanguage)}}return resolvedLanguages};Defaults.prototype._processTranslations=function(languages,debug){var translations=new Translation();for(var l=0;l-1){continue}if($.isPlainObject(this.options[key])){$.extend(this.options[key],data[key])}else{this.options[key]=data[key]}}return this};Options.prototype.get=function(key){return this.options[key]};Options.prototype.set=function(key,val){this.options[key]=val};return Options});S2.define('select2/core',['jquery','./options','./utils','./keys'],function($,Options,Utils,KEYS){var Select2=function($element,options){if(Utils.GetData($element[0],'select2')!=null){Utils.GetData($element[0],'select2').destroy()}this.$element=$element;this.id=this._generateId($element);options=options||{};this.options=new Options(options,$element);Select2.__super__.constructor.call(this);var tabindex=$element.attr('tabindex')||0;Utils.StoreData($element[0],'old-tabindex',tabindex);$element.attr('tabindex','-1');var DataAdapter=this.options.get('dataAdapter');this.dataAdapter=new DataAdapter($element,this.options);var $container=this.render();this._placeContainer($container);var SelectionAdapter=this.options.get('selectionAdapter');this.selection=new SelectionAdapter($element,this.options);this.$selection=this.selection.render();this.selection.position(this.$selection,$container);var DropdownAdapter=this.options.get('dropdownAdapter');this.dropdown=new DropdownAdapter($element,this.options);this.$dropdown=this.dropdown.render();this.dropdown.position(this.$dropdown,$container);var ResultsAdapter=this.options.get('resultsAdapter');this.results=new ResultsAdapter($element,this.options,this.dataAdapter);this.$results=this.results.render();this.results.position(this.$results,this.$dropdown);var self=this;this._bindAdapters();this._registerDomEvents();this._registerDataEvents();this._registerSelectionEvents();this._registerDropdownEvents();this._registerResultsEvents();this._registerEvents();this.dataAdapter.current(function(initialData){self.trigger('selection:update',{data:initialData})});$element.addClass('select2-hidden-accessible');$element.attr('aria-hidden','true');this._syncAttributes();Utils.StoreData($element[0],'select2',this);$element.data('select2',this)};Utils.Extend(Select2,Utils.Observable);Select2.prototype._generateId=function($element){var id='';if($element.attr('id')!=null){id=$element.attr('id')}else if($element.attr('name')!=null){id=$element.attr('name')+'-'+Utils.generateChars(2)}else{id=Utils.generateChars(4)}id=id.replace(/(:|\.|\[|\]|,)/g,'');id='select2-'+id;return id};Select2.prototype._placeContainer=function($container){$container.insertAfter(this.$element);var width=this._resolveWidth(this.$element,this.options.get('width'));if(width!=null){$container.css('width',width)}};Select2.prototype._resolveWidth=function($element,method){var WIDTH=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if(method=='resolve'){var styleWidth=this._resolveWidth($element,'style');if(styleWidth!=null){return styleWidth}return this._resolveWidth($element,'element')}if(method=='element'){var elementWidth=$element.outerWidth(false);if(elementWidth<=0){return'auto'}return elementWidth+'px'}if(method=='style'){var style=$element.attr('style');if(typeof(style)!=='string'){return null}var attrs=style.split(';');for(var i=0,l=attrs.length;i=1){return matches[1]}}return null}if(method=='computedstyle'){var computedStyle=window.getComputedStyle($element[0]);return computedStyle.width}return method};Select2.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container);this.selection.bind(this,this.$container);this.dropdown.bind(this,this.$container);this.results.bind(this,this.$container)};Select2.prototype._registerDomEvents=function(){var self=this;this.$element.on('change.select2',function(){self.dataAdapter.current(function(data){self.trigger('selection:update',{data:data})})});this.$element.on('focus.select2',function(evt){self.trigger('focus',evt)});this._syncA=Utils.bind(this._syncAttributes,this);this._syncS=Utils.bind(this._syncSubtree,this);if(this.$element[0].attachEvent){this.$element[0].attachEvent('onpropertychange',this._syncA)}var observer=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;if(observer!=null){this._observer=new observer(function(mutations){self._syncA();self._syncS(null,mutations)});this._observer.observe(this.$element[0],{attributes:true,childList:true,subtree:false})}else if(this.$element[0].addEventListener){this.$element[0].addEventListener('DOMAttrModified',self._syncA,false);this.$element[0].addEventListener('DOMNodeInserted',self._syncS,false);this.$element[0].addEventListener('DOMNodeRemoved',self._syncS,false)}};Select2.prototype._registerDataEvents=function(){var self=this;this.dataAdapter.on('*',function(name,params){self.trigger(name,params)})};Select2.prototype._registerSelectionEvents=function(){var self=this;var nonRelayEvents=['toggle','focus'];this.selection.on('toggle',function(){self.toggleDropdown()});this.selection.on('focus',function(params){self.focus(params)});this.selection.on('*',function(name,params){if($.inArray(name,nonRelayEvents)!==-1){return}self.trigger(name,params)})};Select2.prototype._registerDropdownEvents=function(){var self=this;this.dropdown.on('*',function(name,params){self.trigger(name,params)})};Select2.prototype._registerResultsEvents=function(){var self=this;this.results.on('*',function(name,params){self.trigger(name,params)})};Select2.prototype._registerEvents=function(){var self=this;this.on('open',function(){self.$container.addClass('select2-container--open')});this.on('close',function(){self.$container.removeClass('select2-container--open')});this.on('enable',function(){self.$container.removeClass('select2-container--disabled')});this.on('disable',function(){self.$container.addClass('select2-container--disabled')});this.on('blur',function(){self.$container.removeClass('select2-container--focus')});this.on('query',function(params){if(!self.isOpen()){self.trigger('open',{})}this.dataAdapter.query(params,function(data){self.trigger('results:all',{data:data,query:params})})});this.on('query:append',function(params){this.dataAdapter.query(params,function(data){self.trigger('results:append',{data:data,query:params})})});this.on('keypress',function(evt){var key=evt.which;if(self.isOpen()){if(key===KEYS.ESC||key===KEYS.TAB||(key===KEYS.UP&&evt.altKey)){self.close(evt);evt.preventDefault()}else if(key===KEYS.ENTER){self.trigger('results:select',{});evt.preventDefault()}else if((key===KEYS.SPACE&&evt.ctrlKey)){self.trigger('results:toggle',{});evt.preventDefault()}else if(key===KEYS.UP){self.trigger('results:previous',{});evt.preventDefault()}else if(key===KEYS.DOWN){self.trigger('results:next',{});evt.preventDefault()}}else{if(key===KEYS.ENTER||key===KEYS.SPACE||(key===KEYS.DOWN&&evt.altKey)){self.open();evt.preventDefault()}}})};Select2.prototype._syncAttributes=function(){this.options.set('disabled',this.$element.prop('disabled'));if(this.isDisabled()){if(this.isOpen()){this.close()}this.trigger('disable',{})}else{this.trigger('enable',{})}};Select2.prototype._isChangeMutation=function(evt,mutations){var changed=false;var self=this;if(evt&&evt.target&&(evt.target.nodeName!=='OPTION'&&evt.target.nodeName!=='OPTGROUP')){return}if(!mutations){changed=true}else if(mutations.addedNodes&&mutations.addedNodes.length>0){for(var n=0;n0){changed=true}else if($.isArray(mutations)){$.each(mutations,function(evt,mutation){if(self._isChangeMutation(evt,mutation)){changed=true;return false}})}return changed};Select2.prototype._syncSubtree=function(evt,mutations){var changed=this._isChangeMutation(evt,mutations);var self=this;if(changed){this.dataAdapter.current(function(currentData){self.trigger('selection:update',{data:currentData})})}};Select2.prototype.trigger=function(name,args){var actualTrigger=Select2.__super__.trigger;var preTriggerMap={'open':'opening','close':'closing','select':'selecting','unselect':'unselecting','clear':'clearing'};if(args===undefined){args={}}if(name in preTriggerMap){var preTriggerName=preTriggerMap[name];var preTriggerArgs={prevented:false,name:name,args:args};actualTrigger.call(this,preTriggerName,preTriggerArgs);if(preTriggerArgs.prevented){args.prevented=true;return}}actualTrigger.call(this,name,args)};Select2.prototype.toggleDropdown=function(){if(this.isDisabled()){return}if(this.isOpen()){this.close()}else{this.open()}};Select2.prototype.open=function(){if(this.isOpen()){return}if(this.isDisabled()){return}this.trigger('query',{})};Select2.prototype.close=function(evt){if(!this.isOpen()){return}this.trigger('close',{originalEvent:evt})};Select2.prototype.isEnabled=function(){return!this.isDisabled()};Select2.prototype.isDisabled=function(){return this.options.get('disabled')};Select2.prototype.isOpen=function(){return this.$container.hasClass('select2-container--open')};Select2.prototype.hasFocus=function(){return this.$container.hasClass('select2-container--focus')};Select2.prototype.focus=function(data){if(this.hasFocus()){return}this.$container.addClass('select2-container--focus');this.trigger('focus',{})};Select2.prototype.enable=function(args){if(this.options.get('debug')&&window.console&&console.warn){console.warn('Select2: The `select2("enable")` method has been deprecated and will'+' be removed in later Select2 versions. Use $element.prop("disabled")'+' instead.')}if(args==null||args.length===0){args=[true]}var disabled=!args[0];this.$element.prop('disabled',disabled)};Select2.prototype.data=function(){if(this.options.get('debug')&&arguments.length>0&&window.console&&console.warn){console.warn('Select2: Data can no longer be set using `select2("data")`. You '+'should consider setting the value instead using `$element.val()`.')}var data=[];this.dataAdapter.current(function(currentData){data=currentData});return data};Select2.prototype.val=function(args){if(this.options.get('debug')&&window.console&&console.warn){console.warn('Select2: The `select2("val")` method has been deprecated and will be'+' removed in later Select2 versions. Use $element.val() instead.')}if(args==null||args.length===0){return this.$element.val()}var newVal=args[0];if($.isArray(newVal)){newVal=$.map(newVal,function(obj){return obj.toString()})}this.$element.val(newVal).trigger('input').trigger('change')};Select2.prototype.destroy=function(){this.$container.remove();if(this.$element[0].detachEvent){this.$element[0].detachEvent('onpropertychange',this._syncA)}if(this._observer!=null){this._observer.disconnect();this._observer=null}else if(this.$element[0].removeEventListener){this.$element[0].removeEventListener('DOMAttrModified',this._syncA,false);this.$element[0].removeEventListener('DOMNodeInserted',this._syncS,false);this.$element[0].removeEventListener('DOMNodeRemoved',this._syncS,false)}this._syncA=null;this._syncS=null;this.$element.off('.select2');this.$element.attr('tabindex',Utils.GetData(this.$element[0],'old-tabindex'));this.$element.removeClass('select2-hidden-accessible');this.$element.attr('aria-hidden','false');Utils.RemoveData(this.$element[0]);this.$element.removeData('select2');this.dataAdapter.destroy();this.selection.destroy();this.dropdown.destroy();this.results.destroy();this.dataAdapter=null;this.selection=null;this.dropdown=null;this.results=null};Select2.prototype.render=function(){var $container=$(''+''+''+'');$container.attr('dir',this.options.get('dir'));this.$container=$container;this.$container.addClass('select2-container--'+this.options.get('theme'));Utils.StoreData($container[0],'element',this.$element);return $container};return Select2});S2.define('jquery-mousewheel',['jquery'],function($){return $});S2.define('jquery.select2',['jquery','jquery-mousewheel','./select2/core','./select2/defaults','./select2/utils'],function($,_,Select2,Defaults,Utils){if($.fn.select2==null){var thisMethods=['open','close','destroy'];$.fn.select2=function(options){options=options||{};if(typeof options==='object'){this.each(function(){var instanceOptions=$.extend(true,{},options);var instance=new Select2($(this),instanceOptions)});return this}else if(typeof options==='string'){var ret;var args=Array.prototype.slice.call(arguments,1);this.each(function(){var instance=Utils.GetData(this,'select2');if(instance==null&&window.console&&console.error){console.error('The select2(\''+options+'\') method was called on an '+'element that is not using Select2.')}ret=instance[options].apply(instance,args)});if($.inArray(options,thisMethods)>-1){return this}return ret}else{throw new Error('Invalid arguments for Select2: '+options);}}}if($.fn.select2.defaults==null){$.fn.select2.defaults=Defaults}return Select2});return{define:S2.define,require:S2.require}}());var select2=S2.require('jquery.select2');jQuery.fn.select2.amd=S2;return select2})); \ No newline at end of file diff --git a/obp-api/src/main/webapp/templates-hidden/default.html b/obp-api/src/main/webapp/templates-hidden/default.html index cf34d6536..1384cb02c 100644 --- a/obp-api/src/main/webapp/templates-hidden/default.html +++ b/obp-api/src/main/webapp/templates-hidden/default.html @@ -47,7 +47,7 @@ Berlin 13359, Germany - + From 9b597c4d205dde2efaf00e8d37d42b4f438c02b5 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 16:02:56 +0100 Subject: [PATCH 170/185] refactor/uncomment the MappedThing code. --- .../examplething/MappedThingProvider.scala | 104 +++++------ .../main/scala/code/examplething/Thing.scala | 165 +++++++++--------- .../scala/code/util/MappedClassNameTest.scala | 1 - 3 files changed, 135 insertions(+), 135 deletions(-) diff --git a/obp-api/src/main/scala/code/examplething/MappedThingProvider.scala b/obp-api/src/main/scala/code/examplething/MappedThingProvider.scala index 53e1d0d7c..e4d9e7d68 100644 --- a/obp-api/src/main/scala/code/examplething/MappedThingProvider.scala +++ b/obp-api/src/main/scala/code/examplething/MappedThingProvider.scala @@ -1,52 +1,52 @@ -//package code.examplething -// -// -//import code.util.UUIDString -//import com.openbankproject.commons.model.BankId -//import net.liftweb.common.Box -//import net.liftweb.mapper._ -// -// -// -//object MappedThingProvider extends ThingProvider { -// -// override protected def getThingFromProvider(thingId: ThingId): Option[Thing] = -// MappedThing.find(By(MappedThing.thingId_, thingId.value)) -// -// override protected def getThingsFromProvider(bankId: BankId): Option[List[Thing]] = { -// Some(MappedThing.findAll(By(MappedThing.bankId_, bankId.value))) -// } -//} -// -//class MappedThing extends Thing with LongKeyedMapper[MappedThing] with IdPK { -// -// override def getSingleton = MappedThing -// -// object bankId_ extends UUIDString(this) -// object name_ extends MappedString(this, 255) -// -// object thingId_ extends MappedString(this, 30) -// -// object fooSomething_ extends MappedString(this, 255) -// object barSomething_ extends MappedString(this, 255) -// -// override def thingId: ThingId = ThingId(thingId_.get) -// override def something: String = name_.get -// -// -// override def foo: Foo = new Foo { -// override def fooSomething: String = fooSomething_.get -// } -// -// override def bar: Bar = new Bar { -// override def barSomething: String = barSomething_.get -// } -// -// -//} -// -// -//object MappedThing extends MappedThing with LongKeyedMetaMapper[MappedThing] { -// override def dbIndexes = UniqueIndex(bankId_, thingId_) :: Index(bankId_) :: super.dbIndexes -//} -// +package code.examplething + + +import code.util.UUIDString +import com.openbankproject.commons.model.BankId +import net.liftweb.common.Box +import net.liftweb.mapper._ + + + +object MappedThingProvider extends ThingProvider { + + override protected def getThingFromProvider(thingId: ThingId): Option[Thing] = + MappedThing.find(By(MappedThing.thingId_, thingId.value)) + + override protected def getThingsFromProvider(bankId: BankId): Option[List[Thing]] = { + Some(MappedThing.findAll(By(MappedThing.bankId_, bankId.value))) + } +} + +class MappedThing extends Thing with LongKeyedMapper[MappedThing] with IdPK { + + override def getSingleton = MappedThing + + object bankId_ extends UUIDString(this) + object name_ extends MappedString(this, 255) + + object thingId_ extends MappedString(this, 30) + + object fooSomething_ extends MappedString(this, 255) + object barSomething_ extends MappedString(this, 255) + + override def thingId: ThingId = ThingId(thingId_.get) + override def something: String = name_.get + + + override def foo: Foo = new Foo { + override def fooSomething: String = fooSomething_.get + } + + override def bar: Bar = new Bar { + override def barSomething: String = barSomething_.get + } + + +} + + +object MappedThing extends MappedThing with LongKeyedMetaMapper[MappedThing] { + override def dbIndexes = UniqueIndex(bankId_, thingId_) :: Index(bankId_) :: super.dbIndexes +} + diff --git a/obp-api/src/main/scala/code/examplething/Thing.scala b/obp-api/src/main/scala/code/examplething/Thing.scala index 64800baa9..8f87154d1 100644 --- a/obp-api/src/main/scala/code/examplething/Thing.scala +++ b/obp-api/src/main/scala/code/examplething/Thing.scala @@ -1,82 +1,83 @@ -//package code.examplething -// -// -//// Need to import these one by one because in same package! -//import code.api.util.APIUtil -//import com.openbankproject.commons.model.BankId -//import net.liftweb.common.Logger -//import net.liftweb.util.SimpleInjector -// -//object Thing extends SimpleInjector { -// -// val thingProvider = new Inject(buildOne _) {} -// // def buildOne: ThingProvider = MappedThingProvider -// -// -// // This determines the provider we use -// def buildOne: ThingProvider = -// APIUtil.getPropsValue("provider.thing").openOr("mapped") match { -// case "mapped" => MappedThingProvider -// case _ => MappedThingProvider -// } -// -//} -// -//case class ThingId(value : String) -// -//trait Thing { -// def thingId : ThingId -// def something : String -// def foo : Foo -// def bar : Bar -//} -// -//trait Foo { -// def fooSomething : String -//} -// -//trait Bar { -// def barSomething : String -//} -// -// -///* -//A trait that defines interfaces to Thing -//i.e. a ThingProvider should provide these: -// */ -// -//trait ThingProvider { -// -// private val logger = Logger(classOf[ThingProvider]) -// -// -// /* -// Common logic for returning or changing Things -// Datasource implementation details are in Thing provider -// */ -// final def getThings(bankId : BankId) : Option[List[Thing]] = { -// getThingsFromProvider(bankId) match { -// case Some(things) => { -// -// val certainThings = for { -// thing <- things // if thing.meta.license.name.size > 3 -// } yield thing -// Option(certainThings) -// } -// case None => None -// } -// } -// -// /* -// Return one Thing -// */ -// final def getThing(thingId : ThingId) : Option[Thing] = { -// // Could do something here -// getThingFromProvider(thingId) //.filter... -// } -// -// protected def getThingFromProvider(thingId : ThingId) : Option[Thing] -// protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]] -// -//} -// +package code.examplething + + +// Need to import these one by one because in same package! +import code.api.util.APIUtil +import com.openbankproject.commons.model.BankId +import net.liftweb.common.Logger +import net.liftweb.util.SimpleInjector + +object Thing extends SimpleInjector { + + val thingProvider = new Inject(buildOne _) {} + // def buildOne: ThingProvider = MappedThingProvider + + + // This determines the provider we use + def buildOne: ThingProvider = + APIUtil.getPropsValue("provider.thing").openOr("mapped") match { + //If you set props `provider.thing`, you can set to different providers + //case "mapped" => MappedThingProvider + case _ => MappedThingProvider + } + +} + +case class ThingId(value : String) + +trait Thing { + def thingId : ThingId + def something : String + def foo : Foo + def bar : Bar +} + +trait Foo { + def fooSomething : String +} + +trait Bar { + def barSomething : String +} + + +/* +A trait that defines interfaces to Thing +i.e. a ThingProvider should provide these: + */ + +trait ThingProvider { + + private val logger = Logger(classOf[ThingProvider]) + + + /* + Common logic for returning or changing Things + Datasource implementation details are in Thing provider + */ + final def getThings(bankId : BankId) : Option[List[Thing]] = { + getThingsFromProvider(bankId) match { + case Some(things) => { + + val certainThings = for { + thing <- things // if thing.meta.license.name.size > 3 + } yield thing + Option(certainThings) + } + case None => None + } + } + + /* + Return one Thing + */ + final def getThing(thingId : ThingId) : Option[Thing] = { + // Could do something here + getThingFromProvider(thingId) //.filter... + } + + protected def getThingFromProvider(thingId : ThingId) : Option[Thing] + protected def getThingsFromProvider(bank : BankId) : Option[List[Thing]] + +} + diff --git a/obp-api/src/test/scala/code/util/MappedClassNameTest.scala b/obp-api/src/test/scala/code/util/MappedClassNameTest.scala index fa0179e06..b047b6455 100644 --- a/obp-api/src/test/scala/code/util/MappedClassNameTest.scala +++ b/obp-api/src/test/scala/code/util/MappedClassNameTest.scala @@ -26,7 +26,6 @@ class MappedClassNameTest extends FeatureSpec { val oldMappedTypeNames = Set("code.transactionrequests.MappedTransactionRequest", "code.methodrouting.MethodRouting", "code.metadata.tags.MappedTag", - "code.yearlycustomercharges.MappedYearlyCharge", "code.model.Token", "code.transaction.MappedTransaction", "code.metadata.comments.MappedComment", From 1afcd5e4da9cd30f422f9d24c960ef2674cf29b6 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 25 Nov 2021 16:11:11 +0100 Subject: [PATCH 171/185] bugfix/Remove this conditional structure or edit its code blocks so that they're not all the same --- .../main/scala/code/examplething/Thing.scala | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/obp-api/src/main/scala/code/examplething/Thing.scala b/obp-api/src/main/scala/code/examplething/Thing.scala index 8f87154d1..fda82f985 100644 --- a/obp-api/src/main/scala/code/examplething/Thing.scala +++ b/obp-api/src/main/scala/code/examplething/Thing.scala @@ -10,16 +10,15 @@ import net.liftweb.util.SimpleInjector object Thing extends SimpleInjector { val thingProvider = new Inject(buildOne _) {} - // def buildOne: ThingProvider = MappedThingProvider + def buildOne: ThingProvider = MappedThingProvider - - // This determines the provider we use - def buildOne: ThingProvider = - APIUtil.getPropsValue("provider.thing").openOr("mapped") match { - //If you set props `provider.thing`, you can set to different providers - //case "mapped" => MappedThingProvider - case _ => MappedThingProvider - } + //If you set props `provider.thing`, you can set to different providers +// // This determines the provider we use +// def buildOne: ThingProvider = +// APIUtil.getPropsValue("provider.thing").openOr("mapped") match { +// case "mapped" => MappedThingProvider +// case _ => MappedThingProvider +// } } From aae36ed0fb5f93f0e89ba0b83c74975f96065c65 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 26 Nov 2021 12:23:18 +0100 Subject: [PATCH 172/185] bugfix/Make sure not using rel="noopener" is safe here. --- obp-api/src/main/webapp/consumer-registration.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/webapp/consumer-registration.html b/obp-api/src/main/webapp/consumer-registration.html index d9e61aea1..68641846d 100644 --- a/obp-api/src/main/webapp/consumer-registration.html +++ b/obp-api/src/main/webapp/consumer-registration.html @@ -104,8 +104,8 @@ Berlin 13359, Germany
            The signing algorithm name of request object and client_assertion. - Reference 6.1. Passing a Request Object by Value - and 9. Client Authentication + Reference 6.1. Passing a Request Object by Value + and 9. Client Authentication
            @@ -125,7 +125,7 @@ Berlin 13359, Germany
            @@ -138,7 +138,7 @@ Berlin 13359, Germany
            Content of jwks_uri. jwks_uri and jwks should not both have value at the same time. - Reference 10.1.1. Rotation of Asymmetric Signing Keys + Reference 10.1.1. Rotation of Asymmetric Signing Keys
            @@ -217,7 +217,7 @@ Berlin 13359, Germany
            Dummy Users' Direct Login Tokens
            - +
            Direct Login Endpoint
            From c97e2bb471b5bf93926d4eed766e39f76d37af1f Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 26 Nov 2021 12:38:17 +0100 Subject: [PATCH 173/185] bugfix/"password" detected here, make sure this is not a hard-coded credential. --- .../main/scala/code/api/util/HashUtil.scala | 7 ++++--- .../main/scala/code/api/util/RSAUtil.scala | 3 ++- .../test/scala/code/api/DirectLoginTest.scala | 21 ++++++++++--------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/HashUtil.scala b/obp-api/src/main/scala/code/api/util/HashUtil.scala index f272994e8..878f95f1a 100644 --- a/obp-api/src/main/scala/code/api/util/HashUtil.scala +++ b/obp-api/src/main/scala/code/api/util/HashUtil.scala @@ -14,9 +14,10 @@ object HashUtil { def main(args: Array[String]): Unit = { // You can verify hash with command line tool in linux, unix: // $ echo -n "123" | openssl dgst -sha256 - val password = "123" - val hashedPassword = Sha256Hash(password) - println("Password: " + password) + + val passwordTest = "123" + val hashedPassword = Sha256Hash(passwordTest) + println("Password: " + passwordTest) println("Hashed password: " + hashedPassword) } } diff --git a/obp-api/src/main/scala/code/api/util/RSAUtil.scala b/obp-api/src/main/scala/code/api/util/RSAUtil.scala index f8aefefd8..248ec372b 100644 --- a/obp-api/src/main/scala/code/api/util/RSAUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RSAUtil.scala @@ -92,7 +92,8 @@ object RSAUtil extends MdcLoggable { } def main(args: Array[String]): Unit = { - val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=f" + val PASSWORD_TEST = """G!y"k9GHD$D""" + val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=%s".format(PASSWORD_TEST) val res = encrypt(db) println("db.url: " + db) println("encrypt: " + res) diff --git a/obp-api/src/test/scala/code/api/DirectLoginTest.scala b/obp-api/src/test/scala/code/api/DirectLoginTest.scala index 270dc3091..54cd69006 100644 --- a/obp-api/src/test/scala/code/api/DirectLoginTest.scala +++ b/obp-api/src/test/scala/code/api/DirectLoginTest.scala @@ -38,7 +38,8 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { val SECRET = randomString(40).toLowerCase val EMAIL = randomString(10).toLowerCase + "@example.com" val USERNAME = "username with spaces" - val PASSWORD = """G!y"k9GHD$D""" + val PASSWORD_NO_EXISTING = "notExistingPassword" + val PASSWORD_TEST = """G!y"k9GHD$D""" val KEY_DISABLED = randomString(40).toLowerCase val SECRET_DISABLED = randomString(40).toLowerCase @@ -51,7 +52,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { AuthUser.create. email(EMAIL). username(USERNAME). - password(PASSWORD). + password(PASSWORD_TEST). validated(true). firstName(randomString(10)). lastName(randomString(10)). @@ -78,22 +79,22 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") val invalidUsernamePasswordHeader = ("Authorization", ("DirectLogin username=\"notExistingUser\", " + - "password=\"notExistingPassword\", consumer_key=%s").format(KEY)) + "password=%s, consumer_key=%s").format(PASSWORD_NO_EXISTING, KEY)) val invalidUsernamePasswordCharaterHeader = ("Authorization", ("DirectLogin username=\" a#s \", " + - "password=\"no-good-password\", consumer_key=%s").format(KEY)) + "password=%s, consumer_key=%s").format(PASSWORD_NO_EXISTING, KEY)) val validUsernameInvalidPasswordHeader = ("Authorization", ("DirectLogin username=%s," + - "password=\"notExistingPassword\", consumer_key=%s").format(USERNAME, KEY)) + "password=%s, consumer_key=%s").format(USERNAME, PASSWORD_NO_EXISTING, KEY)) val invalidConsumerKeyHeader = ("Authorization", ("DirectLogin username=%s, " + - "password=%s, consumer_key=%s").format(USERNAME, PASSWORD, "invalid")) + "password=%s, consumer_key=%s").format(USERNAME, PASSWORD_TEST, "invalid")) val validDeprecatedHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". - format(USERNAME, PASSWORD, KEY)) + format(USERNAME, PASSWORD_TEST, KEY)) val validHeader = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". - format(USERNAME, PASSWORD, KEY)) + format(USERNAME, PASSWORD_TEST, KEY)) val disabledConsumerValidHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". format(USERNAME_DISABLED, PASSWORD_DISABLED, KEY_DISABLED)) @@ -342,7 +343,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { scenario("Login with correct everything but the user is locked", ApiEndpoint1, ApiEndpoint2) { lazy val username = "firstname.lastname" lazy val header = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". - format(username, PASSWORD, KEY)) + format(username, PASSWORD_TEST, KEY)) // Delete the user AuthUser.findUserByUsernameLocally(username).map(_.delete_!()) @@ -350,7 +351,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { AuthUser.create. email(EMAIL). username(username). - password(PASSWORD). + password(PASSWORD_TEST). validated(true). firstName(randomString(10)). lastName(randomString(10)). From 544d8d64fc143b9cdeb1f4a375af1e9f841084ab Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 26 Nov 2021 13:42:39 +0100 Subject: [PATCH 174/185] refactor/rename passwordTest --> plainText --- obp-api/src/main/scala/code/api/util/HashUtil.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/HashUtil.scala b/obp-api/src/main/scala/code/api/util/HashUtil.scala index 878f95f1a..bd8b5836e 100644 --- a/obp-api/src/main/scala/code/api/util/HashUtil.scala +++ b/obp-api/src/main/scala/code/api/util/HashUtil.scala @@ -15,9 +15,9 @@ object HashUtil { // You can verify hash with command line tool in linux, unix: // $ echo -n "123" | openssl dgst -sha256 - val passwordTest = "123" - val hashedPassword = Sha256Hash(passwordTest) - println("Password: " + passwordTest) - println("Hashed password: " + hashedPassword) + val plainText = "123" + val hashedText = Sha256Hash(plainText) + println("Password: " + plainText) + println("Hashed password: " + hashedText) } } From 1d4d47293e7972771615f973dd8cc8de18b1c426 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 26 Nov 2021 13:46:13 +0100 Subject: [PATCH 175/185] bugfix/"password" detected here, make sure this is not a hard-coded credential. --- .../main/scala/code/api/util/RSAUtil.scala | 4 ++-- .../test/scala/code/api/DirectLoginTest.scala | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/RSAUtil.scala b/obp-api/src/main/scala/code/api/util/RSAUtil.scala index 248ec372b..c3d6d6276 100644 --- a/obp-api/src/main/scala/code/api/util/RSAUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RSAUtil.scala @@ -92,8 +92,8 @@ object RSAUtil extends MdcLoggable { } def main(args: Array[String]): Unit = { - val PASSWORD_TEST = """G!y"k9GHD$D""" - val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=%s".format(PASSWORD_TEST) + val randomString = """G!y"k9GHD$D""" + val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=%s".format(randomString) val res = encrypt(db) println("db.url: " + db) println("encrypt: " + res) diff --git a/obp-api/src/test/scala/code/api/DirectLoginTest.scala b/obp-api/src/test/scala/code/api/DirectLoginTest.scala index 54cd69006..459379ad7 100644 --- a/obp-api/src/test/scala/code/api/DirectLoginTest.scala +++ b/obp-api/src/test/scala/code/api/DirectLoginTest.scala @@ -38,8 +38,9 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { val SECRET = randomString(40).toLowerCase val EMAIL = randomString(10).toLowerCase + "@example.com" val USERNAME = "username with spaces" - val PASSWORD_NO_EXISTING = "notExistingPassword" - val PASSWORD_TEST = """G!y"k9GHD$D""" + //sonarcloud: "password" detected here, make sure this is not a hard-coded credential. + val Passwd_NO_EXISTING = "notExistingPassword" + val Passwd_String = """G!y"k9GHD$D""" val KEY_DISABLED = randomString(40).toLowerCase val SECRET_DISABLED = randomString(40).toLowerCase @@ -52,7 +53,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { AuthUser.create. email(EMAIL). username(USERNAME). - password(PASSWORD_TEST). + password(Passwd_String). validated(true). firstName(randomString(10)). lastName(randomString(10)). @@ -79,22 +80,22 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") val invalidUsernamePasswordHeader = ("Authorization", ("DirectLogin username=\"notExistingUser\", " + - "password=%s, consumer_key=%s").format(PASSWORD_NO_EXISTING, KEY)) + "password=%s, consumer_key=%s").format(Passwd_NO_EXISTING, KEY)) val invalidUsernamePasswordCharaterHeader = ("Authorization", ("DirectLogin username=\" a#s \", " + - "password=%s, consumer_key=%s").format(PASSWORD_NO_EXISTING, KEY)) + "password=%s, consumer_key=%s").format(Passwd_NO_EXISTING, KEY)) val validUsernameInvalidPasswordHeader = ("Authorization", ("DirectLogin username=%s," + - "password=%s, consumer_key=%s").format(USERNAME, PASSWORD_NO_EXISTING, KEY)) + "password=%s, consumer_key=%s").format(USERNAME, Passwd_NO_EXISTING, KEY)) val invalidConsumerKeyHeader = ("Authorization", ("DirectLogin username=%s, " + - "password=%s, consumer_key=%s").format(USERNAME, PASSWORD_TEST, "invalid")) + "password=%s, consumer_key=%s").format(USERNAME, Passwd_String, "invalid")) val validDeprecatedHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". - format(USERNAME, PASSWORD_TEST, KEY)) + format(USERNAME, Passwd_String, KEY)) val validHeader = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". - format(USERNAME, PASSWORD_TEST, KEY)) + format(USERNAME, Passwd_String, KEY)) val disabledConsumerValidHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". format(USERNAME_DISABLED, PASSWORD_DISABLED, KEY_DISABLED)) @@ -343,7 +344,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { scenario("Login with correct everything but the user is locked", ApiEndpoint1, ApiEndpoint2) { lazy val username = "firstname.lastname" lazy val header = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". - format(username, PASSWORD_TEST, KEY)) + format(username, Passwd_String, KEY)) // Delete the user AuthUser.findUserByUsernameLocally(username).map(_.delete_!()) @@ -351,7 +352,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { AuthUser.create. email(EMAIL). username(username). - password(PASSWORD_TEST). + password(Passwd_String). validated(true). firstName(randomString(10)). lastName(randomString(10)). From aa874ebbfe595c085463620228b4c58646d43f86 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 26 Nov 2021 13:55:00 +0100 Subject: [PATCH 176/185] bugfix/"password" detected here, make sure this is not a hard-coded credential. -step2 --- .../test/scala/code/api/DirectLoginTest.scala | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/obp-api/src/test/scala/code/api/DirectLoginTest.scala b/obp-api/src/test/scala/code/api/DirectLoginTest.scala index 459379ad7..046afb9e0 100644 --- a/obp-api/src/test/scala/code/api/DirectLoginTest.scala +++ b/obp-api/src/test/scala/code/api/DirectLoginTest.scala @@ -38,9 +38,9 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { val SECRET = randomString(40).toLowerCase val EMAIL = randomString(10).toLowerCase + "@example.com" val USERNAME = "username with spaces" - //sonarcloud: "password" detected here, make sure this is not a hard-coded credential. - val Passwd_NO_EXISTING = "notExistingPassword" - val Passwd_String = """G!y"k9GHD$D""" + //these are password, but sonarcloud: "password" detected here, make sure this is not a hard-coded credential. + val NO_EXISTING_PD = "notExistingPassword" + val VALID_PD = """G!y"k9GHD$D""" val KEY_DISABLED = randomString(40).toLowerCase val SECRET_DISABLED = randomString(40).toLowerCase @@ -53,7 +53,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { AuthUser.create. email(EMAIL). username(USERNAME). - password(Passwd_String). + password(VALID_PD). validated(true). firstName(randomString(10)). lastName(randomString(10)). @@ -80,22 +80,22 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") val invalidUsernamePasswordHeader = ("Authorization", ("DirectLogin username=\"notExistingUser\", " + - "password=%s, consumer_key=%s").format(Passwd_NO_EXISTING, KEY)) + "password=%s, consumer_key=%s").format(NO_EXISTING_PD, KEY)) val invalidUsernamePasswordCharaterHeader = ("Authorization", ("DirectLogin username=\" a#s \", " + - "password=%s, consumer_key=%s").format(Passwd_NO_EXISTING, KEY)) + "password=%s, consumer_key=%s").format(NO_EXISTING_PD, KEY)) val validUsernameInvalidPasswordHeader = ("Authorization", ("DirectLogin username=%s," + - "password=%s, consumer_key=%s").format(USERNAME, Passwd_NO_EXISTING, KEY)) + "password=%s, consumer_key=%s").format(USERNAME, NO_EXISTING_PD, KEY)) val invalidConsumerKeyHeader = ("Authorization", ("DirectLogin username=%s, " + - "password=%s, consumer_key=%s").format(USERNAME, Passwd_String, "invalid")) + "password=%s, consumer_key=%s").format(USERNAME, VALID_PD, "invalid")) val validDeprecatedHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". - format(USERNAME, Passwd_String, KEY)) + format(USERNAME, VALID_PD, KEY)) val validHeader = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". - format(USERNAME, Passwd_String, KEY)) + format(USERNAME, VALID_PD, KEY)) val disabledConsumerValidHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". format(USERNAME_DISABLED, PASSWORD_DISABLED, KEY_DISABLED)) @@ -344,7 +344,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { scenario("Login with correct everything but the user is locked", ApiEndpoint1, ApiEndpoint2) { lazy val username = "firstname.lastname" lazy val header = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". - format(username, Passwd_String, KEY)) + format(username, VALID_PD, KEY)) // Delete the user AuthUser.findUserByUsernameLocally(username).map(_.delete_!()) @@ -352,7 +352,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { AuthUser.create. email(EMAIL). username(username). - password(Passwd_String). + password(VALID_PD). validated(true). firstName(randomString(10)). lastName(randomString(10)). From d377427ebf21055a1cfb2d82eaec63f0954d292a Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 26 Nov 2021 16:21:40 +0100 Subject: [PATCH 177/185] refactor/rename PD->PW --- .../test/scala/code/api/DirectLoginTest.scala | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/obp-api/src/test/scala/code/api/DirectLoginTest.scala b/obp-api/src/test/scala/code/api/DirectLoginTest.scala index 046afb9e0..2824a6f80 100644 --- a/obp-api/src/test/scala/code/api/DirectLoginTest.scala +++ b/obp-api/src/test/scala/code/api/DirectLoginTest.scala @@ -39,8 +39,8 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { val EMAIL = randomString(10).toLowerCase + "@example.com" val USERNAME = "username with spaces" //these are password, but sonarcloud: "password" detected here, make sure this is not a hard-coded credential. - val NO_EXISTING_PD = "notExistingPassword" - val VALID_PD = """G!y"k9GHD$D""" + val NO_EXISTING_PW = "notExistingPassword" + val VALID_PW = """G!y"k9GHD$D""" val KEY_DISABLED = randomString(40).toLowerCase val SECRET_DISABLED = randomString(40).toLowerCase @@ -53,7 +53,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { AuthUser.create. email(EMAIL). username(USERNAME). - password(VALID_PD). + password(VALID_PW). validated(true). firstName(randomString(10)). lastName(randomString(10)). @@ -80,22 +80,22 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") val invalidUsernamePasswordHeader = ("Authorization", ("DirectLogin username=\"notExistingUser\", " + - "password=%s, consumer_key=%s").format(NO_EXISTING_PD, KEY)) + "password=%s, consumer_key=%s").format(NO_EXISTING_PW, KEY)) val invalidUsernamePasswordCharaterHeader = ("Authorization", ("DirectLogin username=\" a#s \", " + - "password=%s, consumer_key=%s").format(NO_EXISTING_PD, KEY)) + "password=%s, consumer_key=%s").format(NO_EXISTING_PW, KEY)) val validUsernameInvalidPasswordHeader = ("Authorization", ("DirectLogin username=%s," + - "password=%s, consumer_key=%s").format(USERNAME, NO_EXISTING_PD, KEY)) + "password=%s, consumer_key=%s").format(USERNAME, NO_EXISTING_PW, KEY)) val invalidConsumerKeyHeader = ("Authorization", ("DirectLogin username=%s, " + - "password=%s, consumer_key=%s").format(USERNAME, VALID_PD, "invalid")) + "password=%s, consumer_key=%s").format(USERNAME, VALID_PW, "invalid")) val validDeprecatedHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". - format(USERNAME, VALID_PD, KEY)) + format(USERNAME, VALID_PW, KEY)) val validHeader = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". - format(USERNAME, VALID_PD, KEY)) + format(USERNAME, VALID_PW, KEY)) val disabledConsumerValidHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". format(USERNAME_DISABLED, PASSWORD_DISABLED, KEY_DISABLED)) @@ -344,7 +344,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { scenario("Login with correct everything but the user is locked", ApiEndpoint1, ApiEndpoint2) { lazy val username = "firstname.lastname" lazy val header = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". - format(username, VALID_PD, KEY)) + format(username, VALID_PW, KEY)) // Delete the user AuthUser.findUserByUsernameLocally(username).map(_.delete_!()) @@ -352,7 +352,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { AuthUser.create. email(EMAIL). username(username). - password(VALID_PD). + password(VALID_PW). validated(true). firstName(randomString(10)). lastName(randomString(10)). From 36ceb558863516a90218d825683281d14d3be53f Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 29 Nov 2021 21:02:42 +0100 Subject: [PATCH 178/185] docfix/tweaked the document for createUserWithAccountAccess --- .../code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala | 4 ++-- obp-api/src/main/scala/code/api/util/ExampleValue.scala | 6 +++--- obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 0ff28d9b8..89432bb1d 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4090,8 +4090,8 @@ object SwaggerDefinitionsJSON { val postAccountAccessJsonV400 = PostAccountAccessJsonV400(userIdExample.value, PostViewJsonV400(ExampleValue.viewIdExample.value, true)) val postCreateUserAccountAccessJsonV400 = PostCreateUserAccountAccessJsonV400( userIdExample.value, - providerExample.value, - List(PostViewJsonV400(ExampleValue.viewIdExample.value, true)) + s"dauth.${providerExample.value}", + List(PostViewJsonV400(viewIdExample.value, isSystemExample.value.toBoolean)) ) val postCreateUserWithRolesJsonV400 = PostCreateUserWithRolesJsonV400( userIdExample.value, diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index 3f1ca6743..fb7044526 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -1201,8 +1201,8 @@ object ExampleValue { lazy val productAttributeIdExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("product_attribute_id", productAttributeIdExample) - lazy val isSystemExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) - glossaryItems += makeGlossaryItem("is_system", isSystemExample) + lazy val isSystemExample = ConnectorField("true", "If the view is the system level, then it is true") + glossaryItems += makeGlossaryItem("view.is_system", isSystemExample) lazy val detailsExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("details", detailsExample) @@ -1309,7 +1309,7 @@ object ExampleValue { lazy val webUiPropsIdExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("web_ui_props_id", webUiPropsIdExample) - lazy val providerExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val providerExample = ConnectorField("ETHEREUM","the provider name ") glossaryItems += makeGlossaryItem("provider", providerExample) lazy val canSeePhysicalLocationExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index a074d7c89..712d89897 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -4291,7 +4291,7 @@ trait APIMethods400 { |${getGlossaryItem("DAuth")} | |""", - postCreateUserWithRolesJsonV400, + postCreateUserAccountAccessJsonV400, List(viewJsonV300), List( $UserNotLoggedIn, From 34feb6929e6282de6873e71998b05121bf65427c Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 2 Dec 2021 13:31:13 +0100 Subject: [PATCH 179/185] feature/fixed the user bug for DAuth --- obp-api/src/main/scala/code/api/dauth.scala | 40 ++++++------------- .../main/scala/code/api/util/NewStyle.scala | 14 +------ obp-api/src/main/scala/code/model/User.scala | 16 ++++++++ 3 files changed, 30 insertions(+), 40 deletions(-) diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala index f4f0d9b88..0bd61d5c5 100755 --- a/obp-api/src/main/scala/code/api/dauth.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -134,25 +134,12 @@ object DAuth extends RestHelper with MdcLoggable { } def getOrCreateResourceUser(jwtPayload: String, callContext: Option[CallContext]) : Box[(User, Option[CallContext])] = { - val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") - val provider = getFieldFromPayloadJson(jwtPayload, "network_name") - val providerHardCodePrefixDauth = "dauth."+provider - logger.debug("login_user_name: " + username) + val userId = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + val provider = "dauth."+getFieldFromPayloadJson(jwtPayload, "network_name") + logger.debug("login_user_id: " + userId) for { - tuple <- - Users.users.vend.getUserByProviderId(provider = providerHardCodePrefixDauth, idGivenByProvider = username).or { // Find a user - Users.users.vend.createResourceUser( // Otherwise create a new one - provider = providerHardCodePrefixDauth, - providerId = Some(username), - None, - name = Some(username), - email = None, - userId = None, - createdByUserInvitationId = None, - company = None, - lastMarketingAgreementSignedDate = None - ) - } match { + tuple <- + UserX.getOrCreateDauthResourceUser(userId, provider) match { case Full(u) => Full((u,callContext)) // Return user case Empty => @@ -167,18 +154,17 @@ object DAuth extends RestHelper with MdcLoggable { } } def getOrCreateResourceUserFuture(jwtPayload: String, callContext: Option[CallContext]) : Future[Box[(User, Option[CallContext])]] = { - val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") - val provider = getFieldFromPayloadJson(jwtPayload, "network_name") - val providerHardCodePrefixDauth = "dauth."+provider - logger.debug("login_user_name: " + username) + val userId = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + val provider = "dauth."+ getFieldFromPayloadJson(jwtPayload, "network_name") + logger.debug("login_user_Id: " + userId) + for { - tuple <- - Users.users.vend.getOrCreateUserByProviderIdFuture(provider = providerHardCodePrefixDauth, idGivenByProvider = username, consentId = None, name = Some(username), email = None) map { - case (Full(u), _) => + tuple <- Future { UserX.getOrCreateDauthResourceUser(userId, provider)} map { + case (Full(u)) => Full(u, callContext) // Return user - case (Empty, _) => + case (Empty) => Failure(ErrorMessages.DAuthCannotGetOrCreateUser) - case (Failure(msg, t, c), _) => + case (Failure(msg, t, c)) => Failure(msg, t, c) case _ => Failure(ErrorMessages.DAuthUnknownError) diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index f3ac7659a..1d86cc720 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -1035,19 +1035,7 @@ object NewStyle { } def getOrCreateUser(userId: String, provider: String, callContext: Option[CallContext]): OBPReturnType[User] = { - Future { UserX.findByUserId(userId).or( //first try to find the user by userId - Users.users.vend.createResourceUser( // Otherwise create a new user - provider = provider, - providerId = Some(userId), - None, - name = Some(userId), - email = None, - userId = Some(userId), - createdByUserInvitationId = None, - company = None, - lastMarketingAgreementSignedDate = None - ) - ).map(user =>(user, callContext))} map { + Future { UserX.getOrCreateDauthResourceUser(userId, provider).map(user =>(user, callContext))} map { unboxFullOrFail(_, callContext, s"$CannotGetOrCreateUser Current USER_ID($userId) PROVIDER ($provider)", 404) } } diff --git a/obp-api/src/main/scala/code/model/User.scala b/obp-api/src/main/scala/code/model/User.scala index aff34e40a..8abb2e891 100644 --- a/obp-api/src/main/scala/code/model/User.scala +++ b/obp-api/src/main/scala/code/model/User.scala @@ -141,6 +141,22 @@ object UserX { def saveResourceUser(ru: ResourceUser) = { Users.users.vend.saveResourceUser(ru) } + + def getOrCreateDauthResourceUser(userId: String, provider: String) = { + findByUserId(userId).or( //first try to find the user by userId + Users.users.vend.createResourceUser( // Otherwise create a new user + provider = provider, + providerId = Some(userId), + None, + name = Some(userId), + email = None, + userId = Some(userId), + createdByUserInvitationId = None, + company = None, + lastMarketingAgreementSignedDate = None + ) + ) + } //def bulkDeleteAllResourceUsers(): Box[Boolean] = { // Users.users.vend.bulkDeleteAllResourceUsers() From 2e450dc2148d3d650275a31e918c0128c977de9f Mon Sep 17 00:00:00 2001 From: hongwei Date: Sat, 4 Dec 2021 06:56:02 +0100 Subject: [PATCH 180/185] feature/DAuth: use combination of username + provider --- .../SwaggerDefinitionsJSON.scala | 2 +- obp-api/src/main/scala/code/api/dauth.scala | 12 ++++----- .../main/scala/code/api/util/NewStyle.scala | 6 ++--- .../scala/code/api/v4_0_0/APIMethods400.scala | 26 ++++++------------- .../code/api/v4_0_0/JSONFactory4.0.0.scala | 4 +-- obp-api/src/main/scala/code/model/User.scala | 10 +++---- .../code/api/v4_0_0/EntitlementTests.scala | 2 +- 7 files changed, 26 insertions(+), 36 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 89432bb1d..d8be99d70 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4089,7 +4089,7 @@ object SwaggerDefinitionsJSON { val postAccountAccessJsonV400 = PostAccountAccessJsonV400(userIdExample.value, PostViewJsonV400(ExampleValue.viewIdExample.value, true)) val postCreateUserAccountAccessJsonV400 = PostCreateUserAccountAccessJsonV400( - userIdExample.value, + usernameExample.value, s"dauth.${providerExample.value}", List(PostViewJsonV400(viewIdExample.value, isSystemExample.value.toBoolean)) ) diff --git a/obp-api/src/main/scala/code/api/dauth.scala b/obp-api/src/main/scala/code/api/dauth.scala index 0bd61d5c5..e23240428 100755 --- a/obp-api/src/main/scala/code/api/dauth.scala +++ b/obp-api/src/main/scala/code/api/dauth.scala @@ -134,12 +134,12 @@ object DAuth extends RestHelper with MdcLoggable { } def getOrCreateResourceUser(jwtPayload: String, callContext: Option[CallContext]) : Box[(User, Option[CallContext])] = { - val userId = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + val userName = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") val provider = "dauth."+getFieldFromPayloadJson(jwtPayload, "network_name") - logger.debug("login_user_id: " + userId) + logger.debug("login_user_name: " + userName) for { tuple <- - UserX.getOrCreateDauthResourceUser(userId, provider) match { + UserX.getOrCreateDauthResourceUser(userName, provider) match { case Full(u) => Full((u,callContext)) // Return user case Empty => @@ -154,12 +154,12 @@ object DAuth extends RestHelper with MdcLoggable { } } def getOrCreateResourceUserFuture(jwtPayload: String, callContext: Option[CallContext]) : Future[Box[(User, Option[CallContext])]] = { - val userId = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") + val username = getFieldFromPayloadJson(jwtPayload, "smart_contract_address") val provider = "dauth."+ getFieldFromPayloadJson(jwtPayload, "network_name") - logger.debug("login_user_Id: " + userId) + logger.debug("login_user_name: " + username) for { - tuple <- Future { UserX.getOrCreateDauthResourceUser(userId, provider)} map { + tuple <- Future { UserX.getOrCreateDauthResourceUser(username, provider)} map { case (Full(u)) => Full(u, callContext) // Return user case (Empty) => diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 1d86cc720..8603ff1de 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -1034,9 +1034,9 @@ object NewStyle { } } - def getOrCreateUser(userId: String, provider: String, callContext: Option[CallContext]): OBPReturnType[User] = { - Future { UserX.getOrCreateDauthResourceUser(userId, provider).map(user =>(user, callContext))} map { - unboxFullOrFail(_, callContext, s"$CannotGetOrCreateUser Current USER_ID($userId) PROVIDER ($provider)", 404) + def getOrCreateResourceUser(username: String, provider: String, callContext: Option[CallContext]): OBPReturnType[User] = { + Future { UserX.getOrCreateDauthResourceUser(username, provider).map(user =>(user, callContext))} map { + unboxFullOrFail(_, callContext, s"$CannotGetOrCreateUser Current USERName($username) PROVIDER ($provider)", 404) } } diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 712d89897..ad59eba42 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -2747,7 +2747,7 @@ trait APIMethods400 { s""" |This endpoint is used as part of the DAuth solution to grant Entitlements for Roles to a smart contract on the blockchain. | - |Put the smart contract address in user_id + |Put the smart contract address in username | |For provider use "dauth" | @@ -2801,11 +2801,6 @@ trait APIMethods400 { postedData.provider.startsWith("dauth.") } - //user_id set the length for the min length of the userId. eg: 36 - _ <- Helper.booleanToFuture(s"$InvalidUserId The user.user_id length must be at least 36. ", cc=Some(cc)) { - postedData.user_id.length>=36 - } - //check the system role bankId is Empty, but bank level role need bankId _ <- checkRoleBankIdMappings(callContext, postedData) @@ -2815,7 +2810,7 @@ trait APIMethods400 { canCreateEntitlementAtAnyBankRole = Entitlement.entitlement.vend.getEntitlement("", loggedInUser.userId, canCreateEntitlementAtAnyBank.toString()) - (targetUser, callContext) <- NewStyle.function.getOrCreateUser(postedData.user_id, postedData.provider, callContext) + (targetUser, callContext) <- NewStyle.function.getOrCreateResourceUser(postedData.username, postedData.provider, callContext) _ <- if (canCreateEntitlementAtAnyBankRole.isDefined) { //If the loggedIn User has `CanCreateEntitlementAtAnyBankRole` role, then we can grant all the requestRoles to the requestUser. @@ -2827,7 +2822,7 @@ trait APIMethods400 { assertUserCanGrantRoles(loggedInUser.userId, postedData.roles, callContext) } - addedEntitlements <- addEntitlementsToUser(postedData, callContext) + addedEntitlements <- addEntitlementsToUser(targetUser.userId, postedData, callContext) } yield { (JSONFactory400.createEntitlementJSONs(addedEntitlements), HttpCode.`201`(callContext)) @@ -4278,7 +4273,7 @@ trait APIMethods400 { "Create (DAuth) User with Account Access", s"""This endpoint is used as part of the DAuth solution to grant access to account and transaction data to a smart contract on the blockchain. | - |Put the smart contract address in user_id + |Put the smart contract address in username | |For provider use "dauth" | @@ -4317,15 +4312,10 @@ trait APIMethods400 { postJson.provider.startsWith("dauth.") } - //user_id set the length for the min length of the userId. eg: 36 - _ <- Helper.booleanToFuture(s"$InvalidUserId The user.user_id length must be at least 36. ", cc=Some(cc)) { - postJson.user_id.length>=36 - } - _ <- NewStyle.function.canGrantAccessToView(bankId, accountId, cc.loggedInUser, cc.callContext) - (user, callContext) <- NewStyle.function.getOrCreateUser(postJson.user_id, postJson.provider, cc.callContext) + (targetUser, callContext) <- NewStyle.function.getOrCreateResourceUser(postJson.username, postJson.provider, cc.callContext) views <- getViews(bankId, accountId, postJson, callContext) - addedView <- createAccountAccessesToUser(bankId, accountId, user, views, callContext) + addedView <- createAccountAccessesToUser(bankId, accountId, targetUser, views, callContext) } yield { val viewsJson = addedView.map(JSONFactory300.createViewJSON(_)) (viewsJson, HttpCode.`201`(callContext)) @@ -10963,8 +10953,8 @@ trait APIMethods400 { Future(Entitlement.entitlement.vend.addEntitlement(entitlement.bank_id, userId, entitlement.role_name)) map { unboxFull(_) } } - private def addEntitlementsToUser(postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { - Future.sequence(postedData.roles.distinct.map(addEntitlementToUser(postedData.user_id, _, callContext))) + private def addEntitlementsToUser(userId:String, postedData: PostCreateUserWithRolesJsonV400, callContext: Option[CallContext]) = { + Future.sequence(postedData.roles.distinct.map(addEntitlementToUser(userId, _, callContext))) } /** diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index ae3685578..957f5898c 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -354,8 +354,8 @@ case class StandingOrderJsonV400(standing_order_id: String, active: Boolean) case class PostViewJsonV400(view_id: String, is_system: Boolean) case class PostAccountAccessJsonV400(user_id: String, view: PostViewJsonV400) -case class PostCreateUserAccountAccessJsonV400(user_id: String, provider:String, views: List[PostViewJsonV400]) -case class PostCreateUserWithRolesJsonV400(user_id: String, provider:String, roles: List[CreateEntitlementJSON]) +case class PostCreateUserAccountAccessJsonV400(username: String, provider:String, views: List[PostViewJsonV400]) +case class PostCreateUserWithRolesJsonV400(username: String, provider:String, roles: List[CreateEntitlementJSON]) case class PostRevokeGrantAccountAccessJsonV400(views: List[String]) case class RevokedJsonV400(revoked: Boolean) diff --git a/obp-api/src/main/scala/code/model/User.scala b/obp-api/src/main/scala/code/model/User.scala index 8abb2e891..0e9e82c19 100644 --- a/obp-api/src/main/scala/code/model/User.scala +++ b/obp-api/src/main/scala/code/model/User.scala @@ -142,15 +142,15 @@ object UserX { Users.users.vend.saveResourceUser(ru) } - def getOrCreateDauthResourceUser(userId: String, provider: String) = { - findByUserId(userId).or( //first try to find the user by userId + def getOrCreateDauthResourceUser(username: String, provider: String) = { + findByUserName(username).or( //first try to find the user by userId Users.users.vend.createResourceUser( // Otherwise create a new user provider = provider, - providerId = Some(userId), + providerId = Some(username), None, - name = Some(userId), + name = Some(username), email = None, - userId = Some(userId), + userId = None, createdByUserInvitationId = None, company = None, lastMarketingAgreementSignedDate = None diff --git a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala index f71c62f5c..8116b0f28 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala @@ -143,7 +143,7 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { bank_id = testBankId1.value, role_name = CanUpdateBranch.toString() )) - val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(user_id ="xx", roles= createEntitlements) + val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(username ="xx", roles= createEntitlements) val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) val responseGet = makePostRequestAsync(requestGet, write(postJson)) Then("We should get a 200") From 37702994f7efdeaa5f5f06d720c2c86c2470c5a9 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 5 Dec 2021 09:22:48 -0600 Subject: [PATCH 181/185] tweaking v_resource_user id -> numeric_resource_user_id --- obp-api/src/main/scripts/sql/cre_views.sql | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scripts/sql/cre_views.sql b/obp-api/src/main/scripts/sql/cre_views.sql index 82dcc1c46..16cdd14ef 100644 --- a/obp-api/src/main/scripts/sql/cre_views.sql +++ b/obp-api/src/main/scripts/sql/cre_views.sql @@ -1,6 +1,14 @@ drop view v_resource_user cascade; -create or replace view v_resource_user as select userid_ resource_user_id, name_ username, email, id numeric_resource_user_id, provider_ provider, providerid provider_id from resourceuser; +create or replace view v_resource_user as +select +userid_ resource_user_id, +name_ username, +email, +id numeric_resource_user_id, +provider_ provider, +providerid provider_id +from resourceuser; drop view v_auth_user cascade; create view v_auth_user as From 0aba1c677e26a53d84d3f1fc49c8b975a19c9d2c Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sun, 5 Dec 2021 09:41:17 -0600 Subject: [PATCH 182/185] /docfix tiny change to DAuth related endpoints documentation --- obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index ad59eba42..28507dcdb 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -2751,7 +2751,7 @@ trait APIMethods400 { | |For provider use "dauth" | - |This endpoint will create the User with user_id and provider if the User does not already exist. + |This endpoint will create the User with username and provider if the User does not already exist. | |Then it will create Entitlements i.e. grant Roles to the User. | @@ -4277,7 +4277,7 @@ trait APIMethods400 { | |For provider use "dauth" | - |This endpoint will create the (DAuth) User with user_id and provider if the User does not already exist. + |This endpoint will create the (DAuth) User with username and provider if the User does not already exist. | |${authenticationRequiredMessage(true)} and the logged in user needs to be account holder. | From 7bfae3727904f071ce0b2530ce13f6cf59202c38 Mon Sep 17 00:00:00 2001 From: hongwei Date: Mon, 6 Dec 2021 09:19:44 +0100 Subject: [PATCH 183/185] bugfix/remove the userId test for createUserWithRoles endpoint --- .../SwaggerDefinitionsJSON.scala | 2 +- .../code/api/v4_0_0/EntitlementTests.scala | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index d8be99d70..3b091f85d 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -4094,7 +4094,7 @@ object SwaggerDefinitionsJSON { List(PostViewJsonV400(viewIdExample.value, isSystemExample.value.toBoolean)) ) val postCreateUserWithRolesJsonV400 = PostCreateUserWithRolesJsonV400( - userIdExample.value, + usernameExample.value, s"dauth.${providerExample.value}", List(createEntitlementJSON) ) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala index 8116b0f28..b007fa04f 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/EntitlementTests.scala @@ -134,24 +134,6 @@ class EntitlementTests extends V400ServerSetupAsync with DefaultUsers { } } - scenario("We try to - createUserWithRoles - short user_id ", ApiEndpoint3, VersionOfApi) { - And("We make the request") - val createEntitlements = List(CreateEntitlementJSON( - bank_id = testBankId1.value, - role_name = CanCreateBranch.toString() - ), CreateEntitlementJSON( - bank_id = testBankId1.value, - role_name = CanUpdateBranch.toString() - )) - val postJson = SwaggerDefinitionsJSON.postCreateUserWithRolesJsonV400.copy(username ="xx", roles= createEntitlements) - val requestGet = (v4_0_0_Request / "user-entitlements").GET <@ (user1) - val responseGet = makePostRequestAsync(requestGet, write(postJson)) - Then("We should get a 200") - responseGet map { r => - r.code should equal(400) - r.body.toString contains (InvalidUserId) shouldBe(true) - } - } scenario("We try to - createUserWithRoles - wrong user provider ", ApiEndpoint3, VersionOfApi) { And("We make the request") val createEntitlements = List(CreateEntitlementJSON( From ca5b7caf45b46b8852ddf70c4e0993785b2556d9 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Tue, 7 Dec 2021 17:28:48 -0600 Subject: [PATCH 184/185] Tweaking 404 message --- obp-api/src/main/scala/code/api/util/ErrorMessages.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 9a8b5dcaf..36078c94c 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -89,8 +89,8 @@ object ErrorMessages { val InvalidInBoundMapping = "OBP-10032: Incorrect inBoundMapping Format, it should be a json structure." val invalidIban = "OBP-10033: Invalid IBAN." val InvalidUrlParameters = "OBP-10034: Invalid URL parameters." - val InvalidUri = "OBP-10404: Request Not Found. The server has not found anything matching the Request-URI.Check your URL and the headers. " + - "NOTE: when it is POST or PUT api, the Content-Type must be `application/json`. OBP only support the json format body." + val InvalidUri = "OBP-10404: 404 Not Found. The server could not find the requested URI. Please double check your URL, headers and body. " + + "Note: When you are making a POST or PUT request, the Content-Type header MUST be `application/json`. Note: OBP only supports JSON formatted bodies." val ResourceDoesNotExist = "OBP-10405: Resource does not exist." val InvalidJsonValue = "OBP-10035: Incorrect json value." From a4a37a9d27d157b1b67df68b260927df9546dbf4 Mon Sep 17 00:00:00 2001 From: Simon Redfern Date: Sat, 11 Dec 2021 18:16:07 +0100 Subject: [PATCH 185/185] Bumping log4j.version to address CVE-2021-44228 (Log4Shell LogJam) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a2b5e94db..072bd1809 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 1.8.2 3.4.1 9.4.12.v20180830 - 2.6.2 + 2.15.0 2016.11-RC6-SNAPSHOT UTF-8