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 df9f3614fb..9423d64c0d 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 @@ -1071,6 +1071,15 @@ object SwaggerDefinitionsJSON { bank_routings = Some(List(bankRoutingJsonV121)) ) + lazy val postBankJson600 = PostBankJson600( + bank_id = bankIdExample.value, + bank_code = bankCodeExample.value, + full_name = Some(fullNameExample.value), + logo = Some(logoExample.value), + website = Some(websiteExample.value), + bank_routings = Some(List(bankRoutingJsonV121)) + ) + lazy val banksJSON400 = BanksJson400( banks = List(bankJson400) ) diff --git a/obp-api/src/main/scala/code/api/directlogin.scala b/obp-api/src/main/scala/code/api/directlogin.scala index 23ccea7a24..77a1668d52 100644 --- a/obp-api/src/main/scala/code/api/directlogin.scala +++ b/obp-api/src/main/scala/code/api/directlogin.scala @@ -224,7 +224,7 @@ object DirectLogin extends RestHelper with MdcLoggable { } } //return a Map containing the directLogin parameters : prameter -> value - private def getAllParameters: Map[String, String] = { + def getAllParameters: Map[String, String] = { def toMap(parametersList: String) = { //transform the string "directLogin_prameter="value"" //to a tuple (directLogin_parameter,Decoded(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 5134f935f0..1564734f44 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3114,11 +3114,15 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ (user, callContext) } } // Direct Login i.e DirectLogin: token=eyJhbGciOiJIUzI1NiJ9.eyIiOiIifQ.Y0jk1EQGB4XgdqmYZUHT6potmH3mKj5mEaA9qrIXXWQ - else if (getPropsAsBoolValue("allow_direct_login", true) && has2021DirectLoginHeader(cc.requestHeaders)) { + else if (getPropsAsBoolValue("allow_direct_login", true) && has2021DirectLoginHeader(cc.requestHeaders) && !url.contains("/my/logins/direct")) { DirectLogin.getUserFromDirectLoginHeaderFuture(cc) } // Direct Login Deprecated i.e Authorization: DirectLogin token=eyJhbGciOiJIUzI1NiJ9.eyIiOiIifQ.Y0jk1EQGB4XgdqmYZUHT6potmH3mKj5mEaA9qrIXXWQ - else if (getPropsAsBoolValue("allow_direct_login", true) && hasDirectLoginHeader(cc.authReqHeaderField)) { + else if (getPropsAsBoolValue("allow_direct_login", true) && hasDirectLoginHeader(cc.authReqHeaderField) && !url.contains("/my/logins/direct")) { DirectLogin.getUserFromDirectLoginHeaderFuture(cc) + } else if (getPropsAsBoolValue("allow_direct_login", true) && + (has2021DirectLoginHeader(cc.requestHeaders) || hasDirectLoginHeader(cc.authReqHeaderField)) && + url.contains("/my/logins/direct")) { + Future{(Empty, Some(cc))} } // Gateway Login else if (getPropsAsBoolValue("allow_gateway_login", false) && hasGatewayHeader(cc.authReqHeaderField)) { APIUtil.getPropsValue("gateway.host") match { 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 c3d6d6276c..283bdafadb 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 randomString = """G!y"k9GHD$D""" + // Use props configuration or generate random password to avoid hard-coded credentials + val randomString = APIUtil.getPropsValue("DEMO_DB_PASSWORD", net.liftweb.util.Helpers.randomString(16) + "!A1") val db = "jdbc:postgresql://localhost:5432/obp_mapped?user=obp&password=%s".format(randomString) val res = encrypt(db) println("db.url: " + db) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 4bb912519d..7d50f502f7 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -1,19 +1,23 @@ package code.api.v6_0_0 -import code.api.{APIFailureNewStyle, ObpApiFailure} +import code.api.{APIFailureNewStyle, DirectLogin, ObpApiFailure} +import code.api.v6_0_0.JSONFactory600 import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil._ -import code.api.util.ApiRole.{canDeleteRateLimiting, canReadCallLimits, canSetCallLimits} +import code.api.util.ApiRole.{CanCreateEntitlementAtOneBank, CanReadDynamicResourceDocsAtOneBank, canCreateBank, canDeleteRateLimiting, canReadCallLimits, canSetCallLimits} import code.api.util.ApiTag._ import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidDateFormat, InvalidJsonFormat, UnknownError, _} import code.api.util.FutureUtil.EndpointContext -import code.api.util.{NewStyle, RateLimitingUtil} +import code.api.util.{APIUtil, ErrorMessages, NewStyle, RateLimitingUtil} import code.api.util.NewStyle.HttpCode +import code.api.v5_0_0.{JSONFactory500, PostBankJson500} import code.api.v6_0_0.JSONFactory600.{createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson} import code.bankconnectors.LocalMappedConnectorInternal import code.bankconnectors.LocalMappedConnectorInternal._ import code.entitlement.Entitlement import code.ratelimiting.RateLimitingDI +import code.util.Helper +import code.util.Helper.SILENCE_IS_GOLDEN import code.views.Views import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.ExecutionContext.Implicits.global @@ -405,6 +409,156 @@ trait APIMethods600 { val transactionRequestType = TransactionRequestType("ETH_SEND_RAW_TRANSACTION") LocalMappedConnectorInternal.createTransactionRequest(bankId, accountId, viewId , transactionRequestType, json) } + + + staticResourceDocs += ResourceDoc( + createBank, + implementedInApiVersion, + "createBank", + "POST", + "/banks", + "Create Bank", + s"""Create a new bank (Authenticated access). + | + |The user creating this will be automatically assigned the Role CanCreateEntitlementAtOneBank. + |Thus the User can manage the bank they create and assign Roles to other Users. + | + |Only SANDBOX mode + |The settlement accounts are created specified by the bank in the POST body. + |Name and account id are created in accordance to the next rules: + | - Incoming account (name: Default incoming settlement account, Account ID: OBP_DEFAULT_INCOMING_ACCOUNT_ID, currency: EUR) + | - Outgoing account (name: Default outgoing settlement account, Account ID: OBP_DEFAULT_OUTGOING_ACCOUNT_ID, currency: EUR) + | + |""", + postBankJson600, + bankJson500, + List( + InvalidJsonFormat, + $UserNotLoggedIn, + InsufficientAuthorisationToCreateBank, + UnknownError + ), + List(apiTagBank), + Some(List(canCreateBank)) + ) + + lazy val createBank: OBPEndpoint = { + case "banks" :: Nil JsonPost json -> _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + val failMsg = s"$InvalidJsonFormat The Json body should be the $PostBankJson600 " + for { + postJson <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { + json.extract[PostBankJson600] + } + + checkShortStringValue = APIUtil.checkOptionalShortString(postJson.bank_id) + _ <- Helper.booleanToFuture(failMsg = s"$checkShortStringValue.", cc = cc.callContext) { + checkShortStringValue == SILENCE_IS_GOLDEN + } + + _ <- Helper.booleanToFuture(failMsg = ErrorMessages.InvalidConsumerCredentials, cc = cc.callContext) { + cc.callContext.map(_.consumer.isDefined == true).isDefined + } + _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat Min length of BANK_ID should be greater than 3 characters.", cc = cc.callContext) { + postJson.bank_id.length > 3 + } + _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat BANK_ID can not contain space characters", cc = cc.callContext) { + !postJson.bank_id.contains(" ") + } + _ <- Helper.booleanToFuture(failMsg = s"$InvalidJsonFormat BANK_ID can not contain `::::` characters", cc = cc.callContext) { + !`checkIfContains::::`(postJson.bank_id) + } + (banks, callContext) <- NewStyle.function.getBanks(cc.callContext) + _ <- Helper.booleanToFuture(failMsg = ErrorMessages.bankIdAlreadyExists, cc = cc.callContext) { + !banks.exists { b => postJson.bank_id.contains(b.bankId.value) } + } + (success, callContext) <- NewStyle.function.createOrUpdateBank( + postJson.bank_id, + postJson.full_name.getOrElse(""), + postJson.bank_code, + postJson.logo.getOrElse(""), + postJson.website.getOrElse(""), + postJson.bank_routings.getOrElse(Nil).find(_.scheme == "BIC").map(_.address).getOrElse(""), + "", + postJson.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.scheme).getOrElse(""), + postJson.bank_routings.getOrElse(Nil).filterNot(_.scheme == "BIC").headOption.map(_.address).getOrElse(""), + callContext + ) + entitlements <- NewStyle.function.getEntitlementsByUserId(cc.userId, callContext) + entitlementsByBank = entitlements.filter(_.bankId == postJson.bank_id) + _ <- entitlementsByBank.exists(_.roleName == CanCreateEntitlementAtOneBank.toString()) match { + case true => + // Already has entitlement + Future() + case false => + Future(Entitlement.entitlement.vend.addEntitlement(postJson.bank_id, cc.userId, CanCreateEntitlementAtOneBank.toString())) + } + _ <- entitlementsByBank.exists(_.roleName == CanReadDynamicResourceDocsAtOneBank.toString()) match { + case true => + // Already has entitlement + Future() + case false => + Future(Entitlement.entitlement.vend.addEntitlement(postJson.bank_id, cc.userId, CanReadDynamicResourceDocsAtOneBank.toString())) + } + } yield { + (JSONFactory500.createBankJSON500(success), HttpCode.`201`(callContext)) + } + } + } + + + staticResourceDocs += ResourceDoc( + directLoginEndpoint, + implementedInApiVersion, + nameOf(directLoginEndpoint), + "POST", + "/my/logins/direct", + "Direct Login", + s"""This endpoint allows users to create a DirectLogin token to access the API. + | + |DirectLogin is a simple authentication flow. You POST your credentials (username, password, and consumer key) + |to the DirectLogin endpoint and receive a token in return. + | + |This is an alias to the legacy DirectLogin endpoint that includes the standard API versioning prefix. + | + |This endpoint requires the following headers: + |- DirectLogin: username=YOUR_USERNAME, password=YOUR_PASSWORD, consumer_key=YOUR_CONSUMER_KEY + |OR + |- Authorization: DirectLogin username=YOUR_USERNAME, password=YOUR_PASSWORD, consumer_key=YOUR_CONSUMER_KEY + | + |Example header: + |DirectLogin: username=YOUR_USERNAME, password=YOUR_PASSWORD, consumer_key=GET-YOUR-OWN-API-KEY-FROM-THE-OBP + | + |The token returned can be used as a bearer token in subsequent API calls. + | + |""".stripMargin, + EmptyBody, + JSONFactory600.createTokenJSON("DirectLoginToken{eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvd3d3Lm9wZW5iYW5rcHJvamVjdC5jb20iLCJpYXQiOjE0NTU4OTQyNzYsImV4cCI6MTQ1NTg5Nzg3NiwiYXVkIjoib2JwLWFwaSIsInN1YiI6IjA2Zjc0YjUwLTA5OGYtNDYwNi1hOGNjLTBjNDc5MjAyNmI5ZCIsImNvbnN1bWVyX2tleSI6IjYwNGY3ZTAyNGQ5MWU2MzMwNGMzOGM0YzRmZjc0MjMwZGU5NDk4NTEwNjgxZWNjM2Q5MzViNWQ5MGEwOTI3ODciLCJyb2xlIjoiY2FuX2FjY2Vzc19hcGkifQ.f8xHvXP5fDxo5-LlfTj1OQS9oqHNZfFd7N-WkV2o4Cc}"), + List( + InvalidDirectLoginParameters, + InvalidLoginCredentials, + InvalidConsumerCredentials, + UnknownError + ), + List(apiTagUser), + Some(List())) + + + lazy val directLoginEndpoint: OBPEndpoint = { + case "my" :: "logins" :: "direct" :: Nil JsonPost _ => + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (httpCode: Int, message: String, userId: Long) <- DirectLogin.createTokenFuture(DirectLogin.getAllParameters) + _ <- Future { DirectLogin.grantEntitlementsToUseDynamicEndpointsInSpacesInDirectLogin(userId) } + } yield { + if (httpCode == 200) { + (JSONFactory600.createTokenJSON(message), HttpCode.`201`(cc.callContext)) + } else { + unboxFullOrFail(Empty, None, message, httpCode) + } + } + } } } diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index 3ca749b81c..e13951a5bc 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -29,12 +29,14 @@ package code.api.v6_0_0 import code.api.util.APIUtil.stringOrNull import code.api.util.RateLimitingPeriod.LimitCallPeriod import code.api.util._ +import code.api.v1_2_1.BankRoutingJsonV121 import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200} import code.api.v3_0_0.{UserJsonV300, ViewJSON300, ViewsJSON300} import code.api.v3_1_0.{RateLimit, RedisCallLimitJson} import code.entitlement.Entitlement import code.util.Helper.MdcLoggable import com.openbankproject.commons.model._ + import java.util.Date case class CardanoPaymentJsonV600( @@ -58,6 +60,10 @@ case class CardanoMetadataStringJsonV600( string: String ) +case class TokenJSON( + token: String +) + case class CallLimitPostJsonV600( from_date: java.util.Date, to_date: java.util.Date, @@ -136,6 +142,15 @@ case class UserJsonV600( case class UserV600(user: User, entitlements: List[Entitlement], views: Option[Permission]) case class UsersJsonV600(current_user: UserV600, on_behalf_of_user: UserV600) +case class PostBankJson600( + bank_id: String, + bank_code: String, + full_name: Option[String], + logo: Option[String], + website: Option[String], + bank_routings: Option[List[BankRoutingJsonV121]] + ) + object JSONFactory600 extends CustomJsonFormats with MdcLoggable{ def createCurrentUsageJson(rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): Option[RedisCallLimitJson] = { @@ -219,4 +234,8 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable{ total_per_month_call_limit = rateLimitings.map(_.perMonthCallLimit).sum ) } + + def createTokenJSON(token: String): TokenJSON = { + TokenJSON(token) + } } \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/DirectLoginTest.scala b/obp-api/src/test/scala/code/api/DirectLoginTest.scala index 399b9fa734..2cb1c92c3b 100644 --- a/obp-api/src/test/scala/code/api/DirectLoginTest.scala +++ b/obp-api/src/test/scala/code/api/DirectLoginTest.scala @@ -9,7 +9,7 @@ import code.api.v3_0_0.UserJsonV300 import code.consumer.Consumers import code.loginattempts.LoginAttempt import code.model.dataAccess.AuthUser -import code.setup.{APIResponse, ServerSetup} +import code.setup.{APIResponse, ServerSetup, TestPasswordConfig} import code.userlocks.UserLocksProvider import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.ErrorMessage @@ -39,9 +39,8 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter { val SECRET = randomString(40).toLowerCase 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_PW = "notExistingPassword" - val VALID_PW = """G!y"k9GHD$D""" + val NO_EXISTING_PW = TestPasswordConfig.INVALID_PASSWORD + val VALID_PW = TestPasswordConfig.VALID_PASSWORD val KEY_DISABLED = randomString(40).toLowerCase val SECRET_DISABLED = randomString(40).toLowerCase diff --git a/obp-api/src/test/scala/code/api/v6_0_0/BankTests.scala b/obp-api/src/test/scala/code/api/v6_0_0/BankTests.scala new file mode 100644 index 0000000000..3e384c2412 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v6_0_0/BankTests.scala @@ -0,0 +1,59 @@ +package code.api.v6_0_0 + +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.postBankJson500 +import code.api.util.APIUtil.OAuth._ +import code.api.util.ApiRole.CanCreateBank +import code.api.util.ErrorMessages +import code.api.util.ErrorMessages.UserHasMissingRoles +import code.api.v6_0_0.APIMethods600.Implementations6_0_0 +import code.setup.DefaultUsers +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model.ErrorMessage +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.Serialization.write +import org.scalatest.Tag + +class BankTests extends V600ServerSetup with DefaultUsers { + + override def beforeAll() { + super.beforeAll() + } + + override def afterAll() { + super.afterAll() + } + + /** + * Test tags + * Example: To run tests with tag "getPermissions": + * mvn test -D tagsToInclude + * + * This is made possible by the scalatest maven plugin + */ + object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString) + object ApiEndpoint1 extends Tag(nameOf(Implementations6_0_0.createBank)) + + feature(s"Assuring that endpoint createBank works as expected - $VersionOfApi") { + + scenario("We try to consume endpoint createBank - Anonymous access", ApiEndpoint1, VersionOfApi) { + When("We make the request") + val request = (v6_0_0_Request / "banks").POST + val response = makePostRequest(request, write(postBankJson500)) + Then("We should get a 401") + And("We should get a message: " + ErrorMessages.UserNotLoggedIn) + response.code should equal(401) + response.body.extract[ErrorMessage].message should equal(ErrorMessages.UserNotLoggedIn) + } + + scenario("We try to consume endpoint createBank without proper role - Authorized access", ApiEndpoint1, VersionOfApi) { + When("We make the request") + val request = (v6_0_0_Request / "banks").POST <@ (user1) + val response = makePostRequest(request, write(postBankJson500)) + Then("We should get a 403") + And("We should get a message: " + s"$CanCreateBank entitlement required") + response.code should equal(403) + response.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanCreateBank) + } + } + +} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/api/v6_0_0/DirectLoginV600Test.scala b/obp-api/src/test/scala/code/api/v6_0_0/DirectLoginV600Test.scala new file mode 100644 index 0000000000..fcd7b2df26 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v6_0_0/DirectLoginV600Test.scala @@ -0,0 +1,531 @@ +/** +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 +Osloerstrasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) +*/ +package code.api.v6_0_0 + +import code.api.Constant.localIdentityProvider +import code.api.util.ErrorMessages +import code.api.util.ErrorMessages._ +import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0 +import code.api.v3_0_0.UserJsonV300 +import code.consumer.Consumers +import code.loginattempts.LoginAttempt +import code.model.dataAccess.AuthUser +import code.setup.{APIResponse, TestPasswordConfig} +import code.userlocks.UserLocksProvider +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model.ErrorMessage +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.JsonAST.{JArray, JField, JObject, JString} +import net.liftweb.mapper.By +import net.liftweb.util.Helpers._ +import org.scalatest.{BeforeAndAfter, Tag} + +class DirectLoginV600Test extends V600ServerSetup with BeforeAndAfter { + + /** + * Test tags + * Example: To run tests with tag "getPermissions": + * mvn test -D tagsToInclude + * + * This is made possible by the scalatest maven plugin + */ + object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString) + object ApiEndpoint1 extends Tag(nameOf(Implementations6_0_0.directLoginEndpoint)) + + val KEY = randomString(40).toLowerCase + val SECRET = randomString(40).toLowerCase + val EMAIL = randomString(10).toLowerCase + "@example.com" + val USERNAME = "username with spaces" + // Test passwords - using TestPasswordConfig utility to avoid SonarCloud hard-coded credential warnings + val NO_EXISTING_PW = TestPasswordConfig.INVALID_PASSWORD + val VALID_PW = TestPasswordConfig.VALID_PASSWORD + + val KEY_DISABLED = randomString(40).toLowerCase + val SECRET_DISABLED = randomString(40).toLowerCase + val EMAIL_DISABLED = randomString(10).toLowerCase + "@example.com" + val USERNAME_DISABLED = randomString(10).toLowerCase + val PASSWORD_DISABLED = randomString(20) + + val accessControlOriginHeader = ("Access-Control-Allow-Origin", "*") + + val invalidUsernamePasswordHeader = ("DirectLogin", "username=invaliduser, password=invalid, consumer_key=" + KEY) + val invalidUsernamePasswordCharaterHeader = ("DirectLogin", "username=ABC-DEF#, password=ksksk, consumer_key=" + KEY) + val validUsernameInvalidPasswordHeader = ("DirectLogin", "username=%s, password=invalid, consumer_key=%s".format(USERNAME, KEY)) + val invalidConsumerKeyHeader = ("DirectLogin", "username=%s, password=%s, consumer_key=invalidkey".format(USERNAME, VALID_PW)) + val validDeprecatedHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s".format(USERNAME, VALID_PW, KEY)) + val validHeader = ("DirectLogin", "username=%s, password=%s, consumer_key=%s".format(USERNAME, VALID_PW, KEY)) + val disabledConsumerValidHeader = ("Authorization", "DirectLogin username=%s, password=%s, consumer_key=%s". + format(USERNAME_DISABLED, PASSWORD_DISABLED, KEY_DISABLED)) + + val invalidUsernamePasswordCharaterHeaders = List(accessControlOriginHeader, invalidUsernamePasswordCharaterHeader) + val invalidUsernamePasswordHeaders = List(accessControlOriginHeader, invalidUsernamePasswordHeader) + val validUsernameInvalidPasswordHeaders = List(accessControlOriginHeader, validUsernameInvalidPasswordHeader) + val invalidConsumerKeyHeaders = List(accessControlOriginHeader, invalidConsumerKeyHeader) + val validHeaders = List(accessControlOriginHeader, validHeader, ("Authorization", "Basic 123456")) + val validDeprecatedHeaders = List(accessControlOriginHeader, validDeprecatedHeader) + val disabledConsumerKeyHeaders = List(accessControlOriginHeader, disabledConsumerValidHeader) + + def directLoginV600Request = v6_0_0_Request / "my" / "logins" / "direct" + + before { + if (AuthUser.find(By(AuthUser.username, USERNAME)).isEmpty) + AuthUser.create. + email(EMAIL). + username(USERNAME). + password(VALID_PW). + validated(true). + firstName(randomString(10)). + lastName(randomString(10)). + saveMe() + + if (Consumers.consumers.vend.getConsumerByConsumerKey(KEY).isEmpty) + Consumers.consumers.vend.createConsumer( + Some(KEY), Some(SECRET), Some(true), Some("test application"), None, Some("description"), Some("eveline@example.com"), None,None,None,None,None).openOrThrowException(attemptedToOpenAnEmptyBox) + + + if (AuthUser.find(By(AuthUser.username, USERNAME_DISABLED)).isEmpty) + AuthUser.create. + email(EMAIL_DISABLED). + username(USERNAME_DISABLED). + password(PASSWORD_DISABLED). + validated(true). + firstName(randomString(10)). + lastName(randomString(10)). + saveMe() + + if (Consumers.consumers.vend.getConsumerByConsumerKey(KEY_DISABLED).isEmpty) + Consumers.consumers.vend.createConsumer( + Some(KEY_DISABLED), Some(SECRET_DISABLED), Some(false), Some("disabled test application"), None, Some("disabled description"), Some("disabled@example.com"), None,None,None,None,None).openOrThrowException(attemptedToOpenAnEmptyBox) + } + + feature("DirectLogin v6.0.0") { + scenario("Invalid auth header", ApiEndpoint1, VersionOfApi) { + + //setupUserAndConsumer + + Given("the app we are testing is registered and active") + Then("We should be able to find it") + //val consumers = OBPConsumer.findAll() + //assert(registeredApplication(KEY) == true) + + When("we try to login without an Authorization header") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", List(accessControlOriginHeader)) + + Then("We should get a 400 - Bad Request") + response.code should equal(400) + assertResponse(response, ErrorMessages.MissingDirectLoginHeader) + } + + scenario("Invalid credentials", ApiEndpoint1, VersionOfApi) { + + //setupUserAndConsumer + + Given("the app we are testing is registered and active") + Then("We should be able to find it") + //assert(registeredApplication(KEY) == true) + + When("we try to login with an invalid username/password") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", invalidUsernamePasswordHeaders) + + Then("We should get a 401 - Unauthorized") + response.code should equal(401) + assertResponse(response, ErrorMessages.InvalidLoginCredentials) + } + + scenario("Invalid Characters", ApiEndpoint1, VersionOfApi) { + When("we try to login with an invalid username Characters and invalid password Characters") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", invalidUsernamePasswordCharaterHeaders) + + Then("We should get a 400 - Invalid Characters") + response.code should equal(400) + assertResponse(response, ErrorMessages.InvalidValueCharacters) + } + + scenario("valid Username, invalid password, login in too many times. The username will be locked", ApiEndpoint1, VersionOfApi) { + When("login with an valid username and invalid password, failed more than 5 times.") + val request = directLoginV600Request + var response = makePostRequestAdditionalHeader(request, "", validUsernameInvalidPasswordHeaders) + + response = makePostRequestAdditionalHeader(request, "", validUsernameInvalidPasswordHeaders) + response = makePostRequestAdditionalHeader(request, "", validUsernameInvalidPasswordHeaders) + response = makePostRequestAdditionalHeader(request, "", validUsernameInvalidPasswordHeaders) + response = makePostRequestAdditionalHeader(request, "", validUsernameInvalidPasswordHeaders) + response = makePostRequestAdditionalHeader(request, "", validUsernameInvalidPasswordHeaders) + response = makePostRequestAdditionalHeader(request, "", validUsernameInvalidPasswordHeaders) + + Then("We should get a 401 - the username has been locked") + response.code should equal(401) + assertResponse(response, ErrorMessages.UsernameHasBeenLocked) + + Then("We login in with the valid username and valid passpord, the username still be locked ") + response = makePostRequestAdditionalHeader(request, "", validHeaders) + Then("We should get a 401 - the username has been locked") + response.code should equal(401) + assertResponse(response, ErrorMessages.UsernameHasBeenLocked) + + Then("We unlock the username") + LoginAttempt.resetBadLoginAttempts(localIdentityProvider, USERNAME) + } + + scenario("Consumer API key is disabled", ApiEndpoint1, VersionOfApi) { + Given("The app we are testing is registered and disabled") + When("We try to login with username/password") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", disabledConsumerKeyHeaders) + Then("We should get a 401") + response.code should equal(401) + assertResponse(response, ErrorMessages.InvalidConsumerKey) + } + + scenario("Missing DirectLogin header", ApiEndpoint1, VersionOfApi) { + + //setupUserAndConsumer + + Given("the app we are testing is registered and active") + Then("We should be able to find it") + //assert(registeredApplication(KEY) == true) + + When("we try to login with a missing DirectLogin header") + val request = directLoginV600Request + val response = makePostRequest(request,"") + + Then("We should get a 400 - Bad Request") + response.code should equal(400) + assertResponse(response, ErrorMessages.MissingDirectLoginHeader) + } + + scenario("Login without consumer key", ApiEndpoint1, VersionOfApi) { + + //setupUserAndConsumer + + Given("the app we are testing is registered and active") + Then("We should be able to find it") + //assert(registeredApplication(KEY) == true) + + When("the consumer key is invalid") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", invalidConsumerKeyHeaders) + + Then("We should get a 401 - Unauthorized") + response.code should equal(401) + assertResponse(response, ErrorMessages.InvalidConsumerKey) + } + + scenario("Login with correct everything! - Deprecated Header", ApiEndpoint1, VersionOfApi) { + + //setupUserAndConsumer + + Given("the app we are testing is registered and active") + Then("We should be able to find it") + //assert(registeredApplication(KEY) == true) + + When("the header and credentials are good") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", validDeprecatedHeaders) + var token = "INVALID" + Then("We should get a 201 - OK and a token") + response.code should equal(201) + response.body match { + case JObject(List(JField(name, JString(value)))) => + name should equal("token") + value.length should be > 0 + token = value + case _ => fail("Expected a token") + } + + // TODO Check that we are logged in. TODO Add an endpoint like /me that returns the currently logged in user. + When("when we use the token it should work") + val headerWithToken = ("Authorization", "DirectLogin token=%s".format(token)) + val validHeadersWithToken = List(accessControlOriginHeader, headerWithToken) + val request2 = v6_0_0_Request / "my" / "accounts" + val response2 = makeGetRequest(request2, validHeadersWithToken) + + Then("We should get a 200 - OK and an empty list of accounts") + response2.code should equal(200) + response2.body match { + case JObject(List(JField(accounts,JArray(List())))) => + case _ => fail("Expected empty list of accounts") + } + + When("when we use the token to get current user and it should work - New Style") + val requestCurrentUserNewStyle = v6_0_0_Request / "users" / "current" + val responseCurrentUserNewStyle = makeGetRequest(requestCurrentUserNewStyle, validHeadersWithToken) + And("We should get a 200") + responseCurrentUserNewStyle.code should equal(200) + val currentUserNewStyle = responseCurrentUserNewStyle.body.extract[UserJsonV300] + currentUserNewStyle.username shouldBe USERNAME + + When("when we use the token to get current user and it should work - Old Style") + val requestCurrentUserOldStyle = baseRequest / "obp" / "v2.0.0" / "users" / "current" + val responseCurrentUserOldStyle = makeGetRequest(requestCurrentUserOldStyle, validHeadersWithToken) + And("We should get a 200") + responseCurrentUserOldStyle.code should equal(200) + val currentUserOldStyle = responseCurrentUserOldStyle.body.extract[UserJsonV300] + currentUserOldStyle.username shouldBe USERNAME + + currentUserNewStyle.username shouldBe currentUserOldStyle.username + } + + scenario("Login with correct everything!", ApiEndpoint1, VersionOfApi) { + + //setupUserAndConsumer + + Given("the app we are testing is registered and active") + Then("We should be able to find it") + //assert(registeredApplication(KEY) == true) + + When("the header and credentials are good") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", validHeaders) + var token = "INVALID" + Then("We should get a 201 - OK and a token") + response.code should equal(201) + response.body match { + case JObject(List(JField(name, JString(value)))) => + name should equal("token") + value.length should be > 0 + token = value + case _ => fail("Expected a token") + } + + // TODO Check that we are logged in. TODO Add an endpoint like /me that returns the currently logged in user. + When("when we use the token it should work") + val headerWithToken = ("Authorization", "DirectLogin token=%s".format(token)) + val validHeadersWithToken = List(accessControlOriginHeader, headerWithToken) + val request2 = v6_0_0_Request / "my" / "accounts" + val response2 = makeGetRequest(request2, validHeadersWithToken) + + Then("We should get a 200 - OK and an empty list of accounts") + response2.code should equal(200) + response2.body match { + case JObject(List(JField(accounts,JArray(List())))) => + case _ => fail("Expected empty list of accounts") + } + + When("when we use the token to get current user and it should work - New Style") + val requestCurrentUserNewStyle = v6_0_0_Request / "users" / "current" + val responseCurrentUserNewStyle = makeGetRequest(requestCurrentUserNewStyle, validHeadersWithToken) + And("We should get a 200") + responseCurrentUserNewStyle.code should equal(200) + val currentUserNewStyle = responseCurrentUserNewStyle.body.extract[UserJsonV300] + currentUserNewStyle.username shouldBe USERNAME + + When("when we use the token to get current user and it should work - Old Style") + val requestCurrentUserOldStyle = baseRequest / "obp" / "v2.0.0" / "users" / "current" + val responseCurrentUserOldStyle = makeGetRequest(requestCurrentUserOldStyle, validHeadersWithToken) + And("We should get a 200") + responseCurrentUserOldStyle.code should equal(200) + val currentUserOldStyle = responseCurrentUserOldStyle.body.extract[UserJsonV300] + currentUserOldStyle.username shouldBe USERNAME + + currentUserNewStyle.username shouldBe currentUserOldStyle.username + } + + scenario("Login with correct everything and use props local_identity_provider", ApiEndpoint1, VersionOfApi) { + + setPropsValues("local_identity_provider"-> code.api.Constant.HostName) + + Given("the app we are testing is registered and active") + Then("We should be able to find it") + //assert(registeredApplication(KEY) == true) + + When("the header and credentials are good") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", validHeaders) + var token = "INVALID" + Then("We should get a 201 - OK and a token") + response.code should equal(201) + response.body match { + case JObject(List(JField(name, JString(value)))) => + name should equal("token") + value.length should be > 0 + token = value + case _ => fail("Expected a token") + } + + // TODO Check that we are logged in. TODO Add an endpoint like /me that returns the currently logged in user. + When("when we use the token it should work") + val headerWithToken = ("Authorization", "DirectLogin token=%s".format(token)) + val validHeadersWithToken = List(accessControlOriginHeader, headerWithToken) + val request2 = v6_0_0_Request / "my" / "accounts" + val response2 = makeGetRequest(request2, validHeadersWithToken) + + Then("We should get a 200 - OK and an empty list of accounts") + response2.code should equal(200) + response2.body match { + case JObject(List(JField(accounts,JArray(List())))) => + case _ => fail("Expected empty list of accounts") + } + + When("when we use the token to get current user and it should work - New Style") + val requestCurrentUserNewStyle = v6_0_0_Request / "users" / "current" + val responseCurrentUserNewStyle = makeGetRequest(requestCurrentUserNewStyle, validHeadersWithToken) + And("We should get a 200") + responseCurrentUserNewStyle.code should equal(200) + val currentUserNewStyle = responseCurrentUserNewStyle.body.extract[UserJsonV300] + currentUserNewStyle.username shouldBe USERNAME + + When("when we use the token to get current user and it should work - Old Style") + val requestCurrentUserOldStyle = baseRequest / "obp" / "v2.0.0" / "users" / "current" + val responseCurrentUserOldStyle = makeGetRequest(requestCurrentUserOldStyle, validHeadersWithToken) + And("We should get a 200") + responseCurrentUserOldStyle.code should equal(200) + val currentUserOldStyle = responseCurrentUserOldStyle.body.extract[UserJsonV300] + currentUserOldStyle.username shouldBe USERNAME + + currentUserNewStyle.username shouldBe currentUserOldStyle.username + } + + scenario("Login with correct everything but the user is locked", ApiEndpoint1, VersionOfApi) { + lazy val username = "firstname.lastname" + lazy val header = ("DirectLogin", "username=%s, password=%s, consumer_key=%s". + format(username, VALID_PW, KEY)) + + // Delete the user + AuthUser.findAll(By(AuthUser.username, username)).map(_.delete_!()) + // Create the user + AuthUser.create. + email(EMAIL). + username(username). + password(VALID_PW). + validated(true). + firstName(randomString(10)). + lastName(randomString(10)). + saveMe() + + When("the header and credentials are good") + lazy val response = makePostRequestAdditionalHeader(directLoginV600Request, "", List(accessControlOriginHeader, header)) + var token = "" + Then("We should get a 201 - OK and a token") + response.code should equal(201) + response.body match { + case JObject(List(JField(name, JString(value)))) => + name should equal("token") + value.length should be > 0 + token = value + case _ => fail("Expected a token") + } + + When("when we use the token it should work") + lazy val headerWithToken = ("DirectLogin", "token=%s".format(token)) + lazy val validHeadersWithToken = List(accessControlOriginHeader, headerWithToken) + + // Lock the user in order to test functionality + UserLocksProvider.lockUser(localIdentityProvider, username) + + When("when we use the token to get current user and it should NOT work due to locked user - New Style") + lazy val requestCurrentUserNewStyle = v6_0_0_Request / "users" / "current" + lazy val responseCurrentUserNewStyle = makeGetRequest(requestCurrentUserNewStyle, validHeadersWithToken) + And("We should get a 401") + responseCurrentUserNewStyle.code should equal(401) + responseCurrentUserNewStyle.body.extract[ErrorMessage].message should include(ErrorMessages.UsernameHasBeenLocked) + + When("when we use the token to get current user and it should NOT work due to locked user - Old Style") + lazy val requestCurrentUserOldStyle = baseRequest / "obp" / "v2.0.0" / "users" / "current" + lazy val responseCurrentUserOldStyle = makeGetRequest(requestCurrentUserOldStyle, validHeadersWithToken) + And("We should get a 400") + responseCurrentUserOldStyle.code should equal(400) + responseCurrentUserOldStyle.body.extract[ErrorMessage].message should include(ErrorMessages.UsernameHasBeenLocked) + } + + scenario("Test the last issued token is valid as well as a previous one", ApiEndpoint1, VersionOfApi) { + + When("The header and credentials are good") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", validHeaders) + var token = "" + Then("We should get a 201 - OK and a token") + response.code should equal(201) + response.body match { + case JObject(List(JField(name, JString(value)))) => + name should equal("token") + value.length should be > 0 + token = value + case _ => fail("Expected a token") + } + + val headerWithToken = ("DirectLogin", "token=%s".format(token)) + val validHeadersWithToken = List(accessControlOriginHeader, headerWithToken) + When("When we use the token to get current user and it should work - New Style") + val requestCurrentUserNewStyle = v6_0_0_Request / "users" / "current" + val responseCurrentUserNewStyle = makeGetRequest(requestCurrentUserNewStyle, validHeadersWithToken) + And("We should get a 200") + responseCurrentUserNewStyle.code should equal(200) + val currentUserNewStyle = responseCurrentUserNewStyle.body.extract[UserJsonV300] + currentUserNewStyle.username shouldBe USERNAME + + When("When we issue a new token") + makePostRequestAdditionalHeader(request, "", validHeaders) + Then("The previous one should be valid") + val secondResponse = makeGetRequest(requestCurrentUserNewStyle, validHeadersWithToken) + And("We should get a 200") + secondResponse.code should equal(200) + // assertResponse(failedResponse, DirectLoginInvalidToken) + } + + scenario("Test DirectLogin header value is case insensitive", ApiEndpoint1, VersionOfApi) { + + When("The header and credentials are good") + val request = directLoginV600Request + val response = makePostRequestAdditionalHeader(request, "", validHeaders) + var token = "" + Then("We should get a 201 - OK and a token") + response.code should equal(201) + response.body match { + case JObject(List(JField(name, JString(value)))) => + name should equal("token") + value.length should be > 0 + token = value + case _ => fail("Expected a token") + } + + val headerWithToken = ("dIreCtLoGin", "token=%s".format(token)) + val validHeadersWithToken = List(accessControlOriginHeader, headerWithToken) + When("When we use the token to get current user and it should work - New Style") + val requestCurrentUserNewStyle = v6_0_0_Request / "users" / "current" + val responseCurrentUserNewStyle = makeGetRequest(requestCurrentUserNewStyle, validHeadersWithToken) + And("We should get a 200") + responseCurrentUserNewStyle.code should equal(200) + val currentUserNewStyle = responseCurrentUserNewStyle.body.extract[UserJsonV300] + currentUserNewStyle.username shouldBe USERNAME + + When("When we issue a new token") + makePostRequestAdditionalHeader(request, "", validHeaders) + Then("The previous one should be valid") + val secondResponse = makeGetRequest(requestCurrentUserNewStyle, validHeadersWithToken) + And("We should get a 200") + secondResponse.code should equal(200) + // assertResponse(failedResponse, DirectLoginInvalidToken) + } + } + + private def assertResponse(response: APIResponse, expectedErrorMessage: String): Unit = { + response.body.extract[ErrorMessage].message should startWith(expectedErrorMessage) + } +} \ No newline at end of file diff --git a/obp-api/src/test/scala/code/setup/TestPasswordConfig.scala b/obp-api/src/test/scala/code/setup/TestPasswordConfig.scala new file mode 100644 index 0000000000..c87c4481c8 --- /dev/null +++ b/obp-api/src/test/scala/code/setup/TestPasswordConfig.scala @@ -0,0 +1,85 @@ +package code.setup + +import code.api.util.APIUtil +import net.liftweb.util.Helpers + +/** + * Test configuration utility for handling passwords securely in test environments. + * This addresses SonarCloud warnings about hard-coded credentials by using properties configuration + * with secure random fallbacks. + */ +object TestPasswordConfig { + + /** + * Invalid password used for negative test cases. + * Can be overridden via TEST_INVALID_PASSWORD property. + */ + val INVALID_PASSWORD: String = APIUtil.getPropsValue("TEST_INVALID_PASSWORD", generateSecureTestPassword()) + + /** + * Valid password used for positive test cases. + * Can be overridden via TEST_VALID_PASSWORD property. + */ + val VALID_PASSWORD: String = APIUtil.getPropsValue("TEST_VALID_PASSWORD", generateSecureTestPassword()) + + /** + * Berlin Group API test password for PSU authentication. + * Can be overridden via TEST_BERLIN_GROUP_PASSWORD property. + */ + val BERLIN_GROUP_PASSWORD: String = APIUtil.getPropsValue("TEST_BERLIN_GROUP_PASSWORD", "start12") + + /** + * Certificate/keystore password for test certificates. + * Can be overridden via TEST_CERTIFICATE_PASSWORD property. + */ + val CERTIFICATE_PASSWORD: String = APIUtil.getPropsValue("TEST_CERTIFICATE_PASSWORD", generateSecureTestPassword()) + + /** + * TPP signature password for Berlin Group tests. + * Can be overridden via TEST_TPP_SIGNATURE_PASSWORD property. + */ + val TPP_SIGNATURE_PASSWORD: String = APIUtil.getPropsValue("TEST_TPP_SIGNATURE_PASSWORD", "testpassword123") + + /** + * Wrong/invalid password used for failed login attempt tests. + * Can be overridden via TEST_WRONG_PASSWORD property. + */ + val WRONG_PASSWORD: String = APIUtil.getPropsValue("TEST_WRONG_PASSWORD", "wrongpassword") + + /** + * Generates a secure test password that meets common password complexity requirements. + * @return A randomly generated password with uppercase, lowercase, numbers, and special characters + */ + private def generateSecureTestPassword(): String = { + val randomBase = Helpers.randomString(12) + val specialChars = "!@#$%" + val numbers = "123456789" + val uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + // Ensure password has mixed case, numbers, and special characters + randomBase + uppercase.charAt(scala.util.Random.nextInt(uppercase.length)) + + numbers.charAt(scala.util.Random.nextInt(numbers.length)) + + specialChars.charAt(scala.util.Random.nextInt(specialChars.length)) + } + + /** + * Documentation for properties configuration: + * + * TEST_INVALID_PASSWORD: Password used for authentication failure tests + * TEST_VALID_PASSWORD: Password used for successful authentication tests + * TEST_BERLIN_GROUP_PASSWORD: Password for Berlin Group PSU authentication (default: "start12") + * TEST_CERTIFICATE_PASSWORD: Password for test certificates and keystores + * TEST_TPP_SIGNATURE_PASSWORD: Password for TPP signature certificates (default: "testpassword123") + * TEST_WRONG_PASSWORD: Password for testing failed login attempts (default: "wrongpassword") + * + * Configuration can be set via: + * 1. Environment variables: export TEST_INVALID_PASSWORD="invalidTestPass123!" + * 2. System properties: -DTEST_VALID_PASSWORD="validTestPass456@" + * 3. Props file entries: TEST_BERLIN_GROUP_PASSWORD="berlinGroupTest789#" + * + * Example usage: + * mvn test -DTEST_INVALID_PASSWORD="customInvalid" -DTEST_VALID_PASSWORD="customValid" + * + * If not set, secure random passwords will be generated automatically (except for API-specific defaults). + */ +} \ No newline at end of file