Merge pull request #1997 from OpenBankProject/develop

regular code cycle
This commit is contained in:
tesobe-daniel 2021-12-23 13:17:48 +01:00 committed by GitHub
commit c3eed98d70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 3904 additions and 87 deletions

View File

@ -20,6 +20,7 @@ Please structure git commit messages in a way as shown below:
4. refactor/Something
5. performance/Something
6. test/Something
7. enhancement/Something
## Code comments

2
NOTICE
View File

@ -1,5 +1,5 @@
Open Bank Project API
Copyright (C) 2011-2019, TESOBE GmbH
Copyright (C) 2011-2021, 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

View File

@ -4,10 +4,46 @@ Look and feel of the API landing page can be modified via css and with a number
## CSS
The default css is located here:
### Main CSS
The main css is located here:
OBP-API/src/main/webapp/media/css/website.css
In case you want to keep it outside of the public code you can specify a new URI via props `webui_main_style_sheet`. For instance:
webui_override_style_sheet = https://static.openbankproject.com/test/css/website.css
### Override CSS
The override css is used if you use `OBP-API/src/main/webapp/media/css/website.css` but you want to override some instance specific values.
In that case you can do it via props `webui_override_style_sheet` i.e.
webui_override_style_sheet = https://static.openbankproject.com/test/css/override.css
where `override.css` could be:
```css
.navbar-default .navbar-nav > li #navitem-logo {
padding-top: 11px;
padding-bottom: 11px;
margin-right: 16px;
height: 88px;
margin-left: 0;
}
.navbar-default .navbar-nav > li #navitem-logo img {
width: 67px;
height: 67px;
}
#main-about {
height: 552px;
}
```
## Props
There's a number of props variables starting with webui_* - see OBP-API/src/main/resources/props/sample.props.template for a comprehensive list and default values.

View File

@ -725,6 +725,7 @@ display_internal_errors=false
# -- OAuth 2 ---------------------------------------------------------------------------------
# Enable/Disable OAuth 2 workflow at a server instance
# In case isn't defined default value is false
# NOTE: Make sure there is no space after the word true/false.
# allow_oauth2_login=false
# URL of Public server JWK set used for validating bearer JWT access tokens
# It can contain more than one URL i.e. list of uris. Values are comma separated.

View File

@ -120,7 +120,7 @@ import code.transactionattribute.MappedTransactionAttribute
import code.transactionrequests.{MappedTransactionRequest, MappedTransactionRequestTypeCharge, TransactionRequestReasons}
import code.usercustomerlinks.MappedUserCustomerLink
import code.userlocks.UserLocks
import code.users.{UserAgreement, UserInvitation}
import code.users.{UserAgreement, UserInitAction, UserInvitation}
import code.util.Helper.MdcLoggable
import code.util.{Helper, HydraUtil}
import code.validation.JsonSchemaValidation
@ -131,7 +131,6 @@ import code.webuiprops.WebUiProps
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.Functions.Implicits._
import com.openbankproject.commons.util.{ApiVersion, Functions}
import javax.mail.{Authenticator, PasswordAuthentication}
import javax.mail.internet.MimeMessage
import net.liftweb.common._
@ -963,7 +962,8 @@ object ToSchemify {
DynamicResourceDoc,
DynamicMessageDoc,
EndpointTag,
ProductFee
ProductFee,
UserInitAction
)++ APIBuilder_Connector.allAPIBuilderModels
// start grpc server

View File

@ -29,7 +29,7 @@ package code.api
import java.util.Date
import code.api.util.APIUtil._
import code.api.util.ErrorMessages.InvalidDirectLoginParameters
import code.api.util.ErrorMessages.{InvalidDirectLoginParameters, attemptedToOpenAnEmptyBox}
import code.api.util.NewStyle.HttpCode
import code.api.util._
import code.consumer.Consumers._
@ -115,6 +115,8 @@ object DirectLogin extends RestHelper with MdcLoggable {
val authUser = AuthUser.findUserByUsernameLocally(resourceUser.name).openOrThrowException(s"$InvalidDirectLoginParameters can not find the auth user!")
AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser)
AuthUser.grantEmailDomainEntitlementsToUser(authUser)
// User init actions
AfterApiAuth.innerLoginUserInitAction(Full(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" );
@ -251,6 +253,10 @@ object DirectLogin extends RestHelper with MdcLoggable {
}
S.request match {
// Recommended header style i.e. DirectLogin: username=s, password=s, consumer_key=s
case Full(a) if a.header("DirectLogin").isDefined == true =>
toMap(a.header("DirectLogin").openOrThrowException(attemptedToOpenAnEmptyBox + " => getAllParameters"))
// Deprecated header style i.e. Authorization: DirectLogin username=s, password=s, consumer_key=s
case Full(a) => a.header("Authorization") match {
case Full(header) => {
if (header.contains("DirectLogin"))
@ -258,11 +264,7 @@ object DirectLogin extends RestHelper with MdcLoggable {
else
Map("error" -> "header incorrect")
}
case _ =>
a.header("DirectLogin") match {
case Full(header) => toMap(header)
case _ => Map("error" -> "missing header")
}
case _ => Map("error" -> "missing header")
}
case _ => Map("error" -> "request incorrect")
}

View File

@ -29,7 +29,7 @@ package code.api
import java.net.HttpURLConnection
import code.api.util.APIUtil._
import code.api.util.{APIUtil, ErrorMessages, JwtUtil}
import code.api.util.{APIUtil, AfterApiAuth, ErrorMessages, JwtUtil}
import code.consumer.Consumers
import code.loginattempts.LoginAttempt
import code.model.{AppType, Consumer}
@ -126,18 +126,20 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable {
AuthUser.grantEmailDomainEntitlementsToUser(authUser)
// Grant roles according to the props email_domain_to_space_mappings
AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(authUser)
// User init actions
AfterApiAuth.innerLoginUserInitAction(Full(authUser))
// Consumer
getOrCreateConsumer(idToken, user.userId) match {
case Full(consumer) =>
saveAuthorizationToken(tokenType, accessToken, idToken, refreshToken, scope, expiresIn, authUser.id.get) match {
case Full(token) => (200, "OK", Some(authUser))
case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotHandleOpenIDConnectData)
case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotHandleOpenIDConnectData+ "saveAuthorizationToken")
case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "saveAuthorizationToken", Some(authUser))
}
case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotHandleOpenIDConnectData)
case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotHandleOpenIDConnectData + "getOrCreateConsumer")
case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "getOrCreateConsumer", Some(authUser))
}
case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotHandleOpenIDConnectData)
case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotHandleOpenIDConnectData + "getOrCreateAuthUser")
case _ => (401, ErrorMessages.CouldNotHandleOpenIDConnectData + "getOrCreateAuthUser", None)
}
case badObj@Failure(_, _, _) => chainErrorMessage(badObj, ErrorMessages.CouldNotSaveOpenIDConnectUser)

View File

@ -161,6 +161,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
def hasDirectLoginHeader(authorization: Box[String]): Boolean = hasHeader("DirectLogin", authorization)
def has2021DirectLoginHeader(requestHeaders: List[HTTPParam]): Boolean = requestHeaders.find(_.name == "DirectLogin").isDefined
def hasAuthorizationHeader(requestHeaders: List[HTTPParam]): Boolean = requestHeaders.find(_.name == "Authorization").isDefined
def hasAnOAuthHeader(authorization: Box[String]): Boolean = hasHeader("OAuth", authorization)
@ -2760,7 +2762,14 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
Future{(cc.user, Some(cc))}
}
else {
Future { (Empty, Some(cc)) }
if(hasAuthorizationHeader(reqHeaders)) {
// We want to throw error in case of wrong or unsupported header. For instance:
// - Authorization: mF_9.B5f-4.1JqM
// - Authorization: Basic mF_9.B5f-4.1JqM
Future { (Failure(ErrorMessages.InvalidAuthorizationHeader), Some(cc)) }
} else {
Future { (Empty, Some(cc)) }
}
}
// COMMON POST AUTHENTICATION CODE GOES BELOW
@ -2769,9 +2778,11 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
val userIsLockedOrDeleted: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkUserIsDeletedOrLocked(res)
// Check Rate Limiting
val resultWithRateLimiting: Future[(Box[User], Option[CallContext])] = AfterApiAuth.checkRateLimiting(userIsLockedOrDeleted)
// User init actions
val resultWithUserInitActions: Future[(Box[User], Option[CallContext])] = AfterApiAuth.outerLoginUserInitAction(resultWithRateLimiting)
// Update Call Context
resultWithRateLimiting map {
resultWithUserInitActions map {
x => (x._1, ApiSession.updateCallContext(Spelling(spelling), x._2))
} map {
x => (x._1, x._2.map(_.copy(implementedInVersion = implementedInVersion)))

View File

@ -2,18 +2,62 @@ package code.api.util
import java.util.Date
import code.api.Constant
import code.api.util.APIUtil.getPropsAsBoolValue
import code.api.util.ApiRole.{CanCreateAccount, CanCreateHistoricalTransactionAtBank}
import code.api.util.ErrorMessages.{UserIsDeleted, UsernameHasBeenLocked}
import code.api.util.RateLimitingJson.CallLimit
import code.bankconnectors.Connector
import code.entitlement.Entitlement
import code.loginattempts.LoginAttempt
import code.model.dataAccess.{AuthUser, MappedBankAccount}
import code.ratelimiting.{RateLimiting, RateLimitingDI}
import com.openbankproject.commons.model.User
import code.users.{UserInitActionProvider, Users}
import code.util.Helper.MdcLoggable
import code.views.Views
import com.openbankproject.commons.model.{AccountId, Bank, BankAccount, User, ViewId}
import net.liftweb.common.{Box, Empty, Failure, Full}
import com.openbankproject.commons.ExecutionContext.Implicits.global
import net.liftweb.mapper.By
import scala.concurrent.Future
object AfterApiAuth {
object AfterApiAuth extends MdcLoggable{
/**
* This function is used to execute actions after an user is authenticated via GUI
* Types of authentication: GUI logon(OpenID Connect and OAuth1.0a)
* @param authUser the authenticated user
*/
def innerLoginUserInitAction(authUser: Box[AuthUser]) = {
authUser.map { u => // Init actions
logger.info("AfterApiAuth.innerLoginUserInitAction started successfully")
sofitInitAction(u)
} match {
case Full(_) => logger.warn("AfterApiAuth.innerLoginUserInitAction completed successfully")
case userInitActionFailure => logger.warn("AfterApiAuth.innerLoginUserInitAction: " + userInitActionFailure)
}
}
/**
* This function is used to execute actions after an user is authenticated via API
* Types of authentication: Direct Login, OpenID Connect, OAuth1.0a, Direct Login, DAuth and Gateway Login
*/
def outerLoginUserInitAction(result: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = {
logger.info("AfterApiAuth.outerLoginUserInitAction started successfully")
for {
(user: Box[User], cc) <- result
} yield {
user match {
case Full(u) => // There is a user. Apply init actions
val authUser: Box[AuthUser] = AuthUser.find(By(AuthUser.user, u.userPrimaryKey.value))
innerLoginUserInitAction(authUser)
(user, cc)
case userInitActionFailure => // There is no user. Just forward the result.
logger.warn("AfterApiAuth.outerLoginUserInitAction: " + userInitActionFailure)
(user, cc)
}
}
}
def checkUserIsDeletedOrLocked(res: Future[(Box[User], Option[CallContext])]): Future[(Box[User], Option[CallContext])] = {
for {
(user: Box[User], cc) <- res
@ -87,4 +131,75 @@ object AfterApiAuth {
}
}
private def sofitInitAction(user: AuthUser): Boolean = applyAction("sofit.logon_init_action.enabled") {
def getOrCreateBankAccount(bank: Bank, accountId: String, label: String, accountType: String = ""): Box[BankAccount] = {
MappedBankAccount.find(
By(MappedBankAccount.bank, bank.bankId.value),
By(MappedBankAccount.theAccountId, accountId)
) match {
case Full(bankAccount) => Full(bankAccount)
case _ =>
val account = Connector.connector.vend.createSandboxBankAccount(
bankId = bank.bankId, accountId = AccountId(accountId), accountNumber = label + "-1",
accountType = accountType, accountLabel = s"$label",
currency = "EUR", initialBalance = 0, accountHolderName = user.username.get,
"",
List.empty
)
if(account.isEmpty) logger.warn(s"AfterApiAuth.sofitInitAction. Cannot create the $label: account for user." + user.firstName + " " + user.lastName)
account
}
}
Users.users.vend.getUserByResourceUserId(user.user.get) match {
case Full(resourceUser) =>
// Create a bank according to the rule: bankid = user.user_id
val bankId = "user." + resourceUser.userId
Connector.connector.vend.createOrUpdateBank(
bankId = bankId,
fullBankName = "user." + resourceUser.userId,
shortBankName = "user." + resourceUser.userId,
logoURL = "",
websiteURL = "",
swiftBIC = "",
national_identifier = "",
bankRoutingScheme = "USER_ID",
bankRoutingAddress = resourceUser.userId
) match {
case Full(bank) =>
UserInitActionProvider.createOrUpdateInitAction(resourceUser.userId, "create-or-update-bank", bankId, true)
// Add roles
val addCanCreateAccount = Entitlement.entitlement.vend.getEntitlement(bank.bankId.value, resourceUser.userId, CanCreateAccount.toString()).or {
Entitlement.entitlement.vend.addEntitlement(bank.bankId.value, resourceUser.userId, CanCreateAccount.toString())
}.isDefined
UserInitActionProvider.createOrUpdateInitAction(resourceUser.userId, "add-entitlement", CanCreateAccount.toString(), addCanCreateAccount)
val addCanCreateHistoricalTransactionAtBank = Entitlement.entitlement.vend.getEntitlement(bank.bankId.value, resourceUser.userId, CanCreateHistoricalTransactionAtBank.toString()).or {
Entitlement.entitlement.vend.addEntitlement(bank.bankId.value, resourceUser.userId, CanCreateHistoricalTransactionAtBank.toString())
}.isDefined
UserInitActionProvider.createOrUpdateInitAction(resourceUser.userId, "add-entitlement", CanCreateHistoricalTransactionAtBank.toString(), addCanCreateHistoricalTransactionAtBank)
// Create Cash account
val bankAccount = getOrCreateBankAccount(bank, "cash", "cash-flow").flatMap( account =>
Views.views.vend.systemView(ViewId(Constant.SYSTEM_OWNER_VIEW_ID)).flatMap( view =>
// Grant account access
Views.views.vend.grantAccessToSystemView(bank.bankId, account.accountId, view, resourceUser)
)
).isDefined
UserInitActionProvider.createOrUpdateInitAction(resourceUser.userId, "add-bank-account", "cache", bankAccount)
addCanCreateAccount && addCanCreateHistoricalTransactionAtBank && bankAccount
case _ =>
logger.warn("AfterApiAuth.sofitInitAction. Cannot create the bank: user." + resourceUser.userId)
UserInitActionProvider.createOrUpdateInitAction(resourceUser.userId, "createOrUpdateBank", bankId, false)
false
}
case _ =>
logger.warn("AfterApiAuth.sofitInitAction. Cannot find resource user by primary key: " + user.id.get)
false
}
}
private def applyAction(propsName: String)(blockOfCode: => Boolean): Boolean = {
val enabled = getPropsAsBoolValue(propsName, false)
if(enabled) blockOfCode else false
}
}

View File

@ -688,10 +688,16 @@ object ApiRole {
lazy val canDeleteTransactionCascade = CanDeleteTransactionCascade()
case class CanDeleteAccountCascade(requiresBankId: Boolean = true) extends ApiRole
lazy val canDeleteAccountCascade = CanDeleteAccountCascade()
lazy val canDeleteAccountCascade = CanDeleteAccountCascade()
case class CanDeleteBankCascade(requiresBankId: Boolean = true) extends ApiRole
lazy val canDeleteBankCascade = CanDeleteBankCascade()
case class CanDeleteProductCascade(requiresBankId: Boolean = true) extends ApiRole
lazy val canDeleteProductCascade = CanDeleteProductCascade()
lazy val canDeleteProductCascade = CanDeleteProductCascade()
case class CanDeleteCustomerCascade(requiresBankId: Boolean = true) extends ApiRole
lazy val canDeleteCustomerCascade = CanDeleteCustomerCascade()
case class CanGetConnectorEndpoint(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetConnectorEndpoint = CanGetConnectorEndpoint()

View File

@ -190,6 +190,9 @@ object ErrorMessages {
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 InvalidAuthorizationHeader = "OBP-20080: Authorization Header format is not supported at this instance."
val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements: "
val CannotGetOrCreateUser = "OBP-20102: Cannot get or create user."
@ -413,6 +416,7 @@ object ErrorMessages {
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 EntitlementCannotBeGrantedGrantorIssue = "OBP-30221: Entitlement cannot be granted due to the grantor's insufficient privileges."
val CreateSystemViewError = "OBP-30250: Could not create the system view"
val DeleteSystemViewError = "OBP-30251: Could not delete the system view"

View File

@ -1958,10 +1958,10 @@ trait APIMethods200 {
_ <- Helper.booleanToFuture(failMsg = if (ApiRole.valueOf(postedData.role_name).requiresBankId) EntitlementIsBankRole else EntitlementIsSystemRole, cc=callContext) {
ApiRole.valueOf(postedData.role_name).requiresBankId == postedData.bank_id.nonEmpty
}
allowedEntitlements = canCreateEntitlementAtOneBank :: canCreateEntitlementAtAnyBank :: Nil
allowedEntitlementsTxt = UserNotSuperAdmin +" or" + UserHasMissingRoles + canCreateEntitlementAtOneBank + s" BankId(${postedData.bank_id})." + " or" + UserHasMissingRoles + canCreateEntitlementAtAnyBank
requiredEntitlements = canCreateEntitlementAtOneBank :: canCreateEntitlementAtAnyBank :: Nil
requiredEntitlementsTxt = UserNotSuperAdmin +" or" + UserHasMissingRoles + canCreateEntitlementAtOneBank + s" BankId(${postedData.bank_id})." + " or" + UserHasMissingRoles + canCreateEntitlementAtAnyBank
_ <- if(isSuperAdmin(u.userId)) Future.successful(Full(Unit))
else NewStyle.function.hasAtLeastOneEntitlement(allowedEntitlementsTxt)(postedData.bank_id, u.userId, allowedEntitlements, callContext)
else NewStyle.function.hasAtLeastOneEntitlement(requiredEntitlementsTxt)(postedData.bank_id, u.userId, requiredEntitlements, callContext)
_ <- Helper.booleanToFuture(failMsg = BankNotFound, cc=callContext) {
postedData.bank_id.nonEmpty == false || BankX(BankId(postedData.bank_id), callContext).map(_._1).isEmpty == false

View File

@ -1,5 +1,9 @@
package code.api.v4_0_0
import java.net.URLEncoder
import java.text.SimpleDateFormat
import java.util.{Calendar, Date}
import code.DynamicData.{DynamicData, DynamicDataProvider}
import code.DynamicEndpoint.DynamicEndpointSwagger
import code.accountattribute.AccountAttributeX
@ -9,6 +13,7 @@ import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, _}
import code.api.util.ApiTag._
import code.api.util.ErrorMessages._
import code.api.util.ExampleValue._
import code.api.util.Glossary.getGlossaryItem
import code.api.util.NewStyle.HttpCode
import code.api.util._
import code.api.util.migration.Migration
@ -25,8 +30,8 @@ import code.api.v3_0_0.JSONFactory300
import code.api.v3_1_0._
import code.api.v4_0_0.JSONFactory400._
import code.api.v4_0_0.dynamic.DynamicEndpointHelper.DynamicReq
import code.api.v4_0_0.dynamic.practise.{DynamicEndpointCodeGenerator, PractiseEndpoint}
import code.api.v4_0_0.dynamic._
import code.api.v4_0_0.dynamic.practise.{DynamicEndpointCodeGenerator, PractiseEndpoint}
import code.api.{ChargePolicy, JsonResponseException}
import code.apicollection.MappedApiCollectionsProvider
import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider
@ -41,7 +46,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, ResourceUser}
import code.model.dataAccess.{AuthUser, BankAccountCreation}
import code.model.{toUserExtended, _}
import code.ratelimiting.RateLimitingDI
import code.snippet.{WebUIPlaceholder, WebUITemplate}
@ -51,7 +56,7 @@ import code.transactionrequests.TransactionRequests.TransactionChallengeTypes._
import code.transactionrequests.TransactionRequests.TransactionRequestTypes
import code.transactionrequests.TransactionRequests.TransactionRequestTypes.{apply => _, _}
import code.userlocks.UserLocksProvider
import code.users.{UserAgreement, Users}
import code.users.Users
import code.util.Helper.booleanToFuture
import code.util.{Helper, JsonSchemaUtil}
import code.validation.JsonValidation
@ -64,7 +69,7 @@ import com.openbankproject.commons.model.enums.DynamicEntityOperation._
import com.openbankproject.commons.model.enums.{TransactionRequestStatus, _}
import com.openbankproject.commons.model.{ListResult, _}
import com.openbankproject.commons.util.{ApiVersion, JsonUtils, ScannedApiVersion}
import deletion.{DeleteAccountCascade, DeleteProductCascade, DeleteTransactionCascade}
import deletion._
import net.liftweb.common._
import net.liftweb.http.rest.RestHelper
import net.liftweb.http.{JsonResponse, Req, S}
@ -78,11 +83,6 @@ 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.text.SimpleDateFormat
import java.util.{Calendar, Date}
import code.api.util.Glossary.getGlossaryItem
import scala.collection.immutable.{List, Nil}
import scala.collection.mutable.ArrayBuffer
@ -3888,6 +3888,12 @@ trait APIMethods400 {
|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)
|
|""",
bankJson400,
bankJson400,
@ -7289,6 +7295,41 @@ trait APIMethods400 {
(Full(true), HttpCode.`200`(cc))
}
}
}
staticResourceDocs += ResourceDoc(
deleteBankCascade,
implementedInApiVersion,
nameOf(deleteBankCascade),
"DELETE",
"/management/cascading/banks/BANK_ID",
"Delete Bank Cascade",
s"""Delete a Bank Cascade specified by BANK_ID.
|
|
|${authenticationRequiredMessage(true)}
|
|""",
EmptyBody,
EmptyBody,
List(
$UserNotLoggedIn,
$BankNotFound,
UserHasMissingRoles,
UnknownError
),
List(apiTagBank, apiTagNewStyle),
Some(List(canDeleteBankCascade)))
lazy val deleteBankCascade : OBPEndpoint = {
case "management" :: "cascading" :: "banks" :: BankId(bankId) :: Nil JsonDelete _ => {
cc =>
for {
_ <- Future(DeleteBankCascade.atomicDelete(bankId))
} yield {
(Full(true), HttpCode.`200`(cc))
}
}
}
staticResourceDocs += ResourceDoc(
@ -7327,6 +7368,44 @@ trait APIMethods400 {
}
}
}
staticResourceDocs += ResourceDoc(
deleteCustomerCascade,
implementedInApiVersion,
nameOf(deleteCustomerCascade),
"DELETE",
"/management/cascading/banks/BANK_ID/customers/CUSTOMER_ID",
"Delete Customer Cascade",
s"""Delete a Customer Cascade specified by CUSTOMER_ID.
|
|
|${authenticationRequiredMessage(true)}
|
|""",
EmptyBody,
EmptyBody,
List(
$UserNotLoggedIn,
$BankNotFound,
CustomerNotFoundByCustomerId,
UserHasMissingRoles,
UnknownError
),
List(apiTagCustomer, apiTagNewStyle),
Some(List(canDeleteCustomerCascade)))
lazy val deleteCustomerCascade : OBPEndpoint = {
case "management" :: "cascading" :: "banks" :: BankId(bankId) :: "customers" :: CustomerId(customerId) :: Nil JsonDelete _ => {
cc =>
for {
(_, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId.value, Some(cc))
_ <- Future(DeleteCustomerCascade.atomicDelete(customerId))
} yield {
(Full(true), HttpCode.`200`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
createCounterparty,
@ -11052,8 +11131,8 @@ trait APIMethods400 {
private def createDynamicEndpointMethod(bankId: Option[String], json: JValue, cc: CallContext) = {
for {
(postedJson, openAPI) <- NewStyle.function.tryons(InvalidJsonFormat, 400, cc.callContext) {
//If it is bank level, we manully added /banks/bankId in all the paths:
(postedJson, openAPI) <- NewStyle.function.tryons(InvalidJsonFormat+"The request json is not valid OpenAPIV3.0.x or Swagger 2.0.x Please check it in Swagger Editor or similar tools ", 400, cc.callContext) {
//If it is bank level, we manually added /banks/bankId in all the paths:
val jsonTweakedPath = DynamicEndpointHelper.addedBankToPath(json, bankId)
val swaggerContent = compactRender(jsonTweakedPath)

View File

@ -35,7 +35,8 @@ import java.util
import java.util.regex.Pattern
import java.util.{Date, UUID}
import com.openbankproject.commons.model.enums.DynamicEntityOperation.GET_ALL
import net.liftweb.json.Formats
import io.swagger.v3.oas.models.examples.Example
import net.liftweb.json.{Formats, JBool}
import scala.collection.JavaConverters._
import scala.collection.immutable.List
@ -57,6 +58,60 @@ object DynamicEndpointHelper extends RestHelper {
def isDynamicEntityResponse (serverUrl : String) = serverUrl matches (IsDynamicEntityUrl)
def isMockedResponse (serverUrl : String) = serverUrl matches (IsMockUrlString)
/**
* 1st: we check if it OpenAPI3.0,
* 2rd: if not, we will check Swagger2.0
* other case, we will return ""
* @param openApiJson it can be swagger2.0 or openApi3.0
* @return the openapi
*/
def getOpenApiVersion(openApiJson: String) ={
val jValue = json.parse(openApiJson)
val openApiVersion = jValue \ "openapi"
val swaggerVersion = jValue \ "swagger"
if (openApiVersion != JNothing ) {
openApiVersion.values.toString.trim
} else if (swaggerVersion != JNothing){
swaggerVersion.values.toString.trim
}else{
""
}
}
/**
* 1st: we check if it OpenAPI3.0, we will keep the OpenAPI3.0 format
*
* 2rd: if not, we will change it as Swagger2.0 format
*
*/
def changeOpenApiVersionHost(openApiJson: String, newHost:String) ={
//for this case, there is no host/servers object, we will add the object
//https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/api-with-examples.json
val openApiVersion = getOpenApiVersion(openApiJson)
val openApiJValue = json.parse(openApiJson)
val serversField = openApiJValue \ "servers"
val hostField = openApiJValue \ "host"
if (openApiVersion.startsWith("3.") && serversField != JNothing) {
json.compactRender(openApiJValue.replace("servers"::Nil, JArray(List(JObject(List(JField("url",newHost)))))))
} else if (openApiVersion.startsWith("3.") && serversField == JNothing) {
val newServers = json.parse(s"""{
| "servers": [
| {
| "url": "$newHost"
| }
| ]
|}""".stripMargin)
json.compactRender(openApiJValue merge newServers)
} else if(hostField != JNothing){
json.compactRender(openApiJValue.replace("host" :: Nil, JString(newHost)))
} else {
val host = json.parse(s"""{"host": "$newHost"}""".stripMargin)
json.compactRender(openApiJValue merge host)
}
}
private def dynamicEndpointInfos: List[DynamicEndpointInfo] = {
val dynamicEndpoints: List[DynamicEndpointT] = DynamicEndpointProvider.connectorMethodProvider.vend.getAll(None)
@ -148,10 +203,22 @@ object DynamicEndpointHelper extends RestHelper {
val mockResponse: Option[(Int, JValue)] = (serverUrl, doc.successResponseBody) match {
case (IsMockUrl(), v: PrimaryDataBody[_]) =>
Some(code -> v.toJValue)
//If the openAPI json do not have response body, we return true as default
val response = if (v.toJValue == JNothing) {
JBool(true)
} else{
v.toJValue
}
Some(code -> response)
case (IsMockUrl(), v: JValue) =>
Some(code -> v)
//If the openAPI json do not have response body, we return true as default
val response = if (v == JNothing) {
JBool(true)
} else{
v
}
Some(code -> response)
case (IsMockUrl(), v) =>
Some(code -> json.Extraction.decompose(v))
@ -196,7 +263,14 @@ object DynamicEndpointHelper extends RestHelper {
}
val paths: mutable.Map[String, PathItem] = openAPI.getPaths.asScala
def entitlementSuffix(path: String) = Math.abs(path.hashCode).toString.substring(0, 3) // to avoid different swagger have same entitlement
def entitlementSuffix(path: String) = {
val pathHashCode = Math.abs(path.hashCode).toString
//eg: path can be "/" --> "/".hashCode => 47, the length is only 2, we need to prepare the worst case:
if(pathHashCode.length>3)
pathHashCode.substring(0, 3)
else
pathHashCode.substring(0, 2)
} // to avoid different swagger have same entitlement
val dynamicEndpointItems: mutable.Iterable[DynamicEndpointItem] = for {
(path, pathItem) <- paths
(method: HttpMethod, op: Operation) <- pathItem.readOperationsMap.asScala
@ -216,6 +290,9 @@ object DynamicEndpointHelper extends RestHelper {
s"""
|
|MethodRouting settings example:
|
|<details>
|
|```
|{
| "is_bank_id_exact_match":false,
@ -239,6 +316,7 @@ object DynamicEndpointHelper extends RestHelper {
|}
|```
|
|</details>
|""".stripMargin
val exampleRequestBody: Product = getRequestExample(openAPI, op.getRequestBody)
val (successCode, successResponseBody: Product) = getResponseExample(openAPI, op.getResponses)
@ -389,6 +467,12 @@ object DynamicEndpointHelper extends RestHelper {
successResponse.flatMap(it => getMediaType(it.getContent))
}
maybeMediaType match {
// https://github.com/OAI/OpenAPI-Specification/blob/3.0.1/versions/3.0.1.md#mediaTypeObject
// following rule is also valid in Swagger UI using this json (object foo)
//: https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/api-with-examples.json
// if schema is not null, then it has the 1st priority
// if schema is null, 2rd priority is examples.
// if schema is null and examples is null, 3rd priority is example field.
case Some(mediaType) if mediaType.getSchema() != null =>
val schema = mediaType.getSchema()
if(schema.isInstanceOf[ArraySchema]) {
@ -400,6 +484,15 @@ object DynamicEndpointHelper extends RestHelper {
.map(getName)
.orNull
}
case Some(mediaType) if mediaType.getExamples() != null =>{
val examples: util.Map[String, Example] = mediaType.getExamples()
val objectName: Option[String] = examples.keySet().asScala.headOption
objectName.getOrElse(examples.values().toString)
}
case Some(mediaType) if mediaType.getExample() != null =>{
val example: AnyRef = mediaType.getExample()
example.toString //TODO, here better set a default value? or can get name from the object(but it depends on the input)
}
case None => null
}
}
@ -416,10 +509,34 @@ object DynamicEndpointHelper extends RestHelper {
getExample(openAPI, schema)
} else {
//body.content is `REQUIRED` field
val mediaType = getMediaType(body.getContent())
assert(mediaType.isDefined, s"RequestBody $body have no MediaType of 'application/json', 'application/x-www-form-urlencoded', 'multipart/form-data' or '*/*'")
val schema = mediaType.get.getSchema
getExample(openAPI, schema)
// https://github.com/OAI/OpenAPI-Specification/blob/3.0.1/versions/3.0.1.md#mediaTypeObject
// following rule is also valid in Swagger UI using this json (object foo)
//: https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/api-with-examples.json
// if schema is not null, then it has the 1st priority
// if schema is null, 2rd priority is examples.
// if schema is null and examples is null, 3rd priority is example field.
if (mediaType.get.getSchema != null)
getExample(openAPI, mediaType.get.getSchema)
else if (body!=null
&& body.getContent != null
&& body.getContent.values().size()>0
&& body.getContent.values().asScala.head.getExamples != null
&& body.getContent.values().asScala.head.getExamples.values().size() > 0
) {
val examplesValue = body.getContent.values().asScala.map(_.getExamples.values().asScala.map(_.getValue.toString)).map(_.head)
convertToProduct(json.parse(examplesValue.head))
} else if(body!=null
&& body.getContent != null
&& body.getContent.values().size()>0
&& body.getContent.values().asScala.head.getExample != null
) {
val exampleValue = body.getContent.values().asScala.map(_.getExample.toString)
convertToProduct(json.parse(exampleValue.head))
}else
EmptyBody
}
}
@ -449,7 +566,32 @@ object DynamicEndpointHelper extends RestHelper {
val result: Option[(Int, Product)] = for {
(code, response) <- successResponse
schema <- getResponseSchema(openAPI, response)
example = getExample(openAPI, schema)
// https://github.com/OAI/OpenAPI-Specification/blob/3.0.1/versions/3.0.1.md#mediaTypeObject
// following rule is also valid in Swagger UI using this json (object foo)
//: https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/api-with-examples.json
// if schema is not null, then it has the 1st priority
// if schema is null, 2rd priority is examples.
// if schema is null and examples is null, 3rd priority is example field.
example = if (schema != null)
getExample(openAPI, schema)
else if (response!=null
&& response.getContent != null
&& response.getContent.values().size()>0
&& response.getContent.values().asScala.head.getExamples != null
&& response.getContent.values().asScala.head.getExamples.values().size() > 0
) {
val examplesValue = response.getContent.values().asScala.map(_.getExamples.values().asScala.map(_.getValue.toString)).map(_.head)
convertToProduct(json.parse(examplesValue.head))
} else if(response!=null
&& response.getContent != null
&& response.getContent.values().size()>0
&& response.getContent.values().asScala.head.getExample != null
) {
val exampleValue = response.getContent.values().asScala.map(_.getExample.toString)
convertToProduct(json.parse(exampleValue.head))
}
else
EmptyBody
} yield code -> example
result
@ -595,7 +737,28 @@ object DynamicEndpointHelper extends RestHelper {
case v: Schema[_] if StringUtils.isNotBlank(v.get$ref()) =>
val refSchema = getRefSchema(openAPI, v.get$ref())
convertToProduct(rec(refSchema))
//For OpenAPI30, have some default object, which do not have any ref.
case v: Schema[_] if StringUtils.isNotBlank(v.getDescription) =>
getDefaultValue(v, v.getDescription)
//https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/petstore-expanded.json
//added this case according to the up swagger, it has `allOf`
case v: ComposedSchema =>{
if (v.getAllOf != null && v.getAllOf.size() >0) {
v.getAllOf.asScala.map(rec(_))
.filter(_.!=(null))
.filter(_.isInstanceOf[JObject])
.map(_.asInstanceOf[JObject])
.reduceLeft(_ merge _)
} else if (v.getAnyOf != null && v.getAnyOf.size()>0){
rec(v.getAllOf.asScala.head)
}else if(v.getOneOf != null && v.getOneOf.size()>0){
rec(v.getOneOf.asScala.head)
}else{
EmptyBody
}
}
case v if v.getType() == "string" => "string"
case _ => throw new RuntimeException(s"Not support type $schema, please support it if necessary.")
}

View File

@ -295,6 +295,9 @@ object DynamicEntityHelper {
private def methodRoutingExample(entityName: String) =
s"""
|MethodRouting settings example:
|
|<details>
|
|```
|{
| "is_bank_id_exact_match":false,
@ -313,6 +316,8 @@ object DynamicEntityHelper {
| ]
|}
|```
|
|</details>
|""".stripMargin
}

View File

@ -3,6 +3,7 @@ package code.DynamicEndpoint
import java.util.UUID.randomUUID
import code.api.cache.Caching
import code.api.util.{APIUtil, CustomJsonFormats}
import code.api.v4_0_0.dynamic.DynamicEndpointHelper
import code.util.MappedUUID
import com.tesobe.CacheKeyFromArguments
import net.liftweb.common.Box
@ -18,7 +19,7 @@ object MappedDynamicEndpointProvider extends DynamicEndpointProvider with Custom
val dynamicEndpointTTL : Int = {
if(Props.testMode) 0
else //Better set this to 0, we maybe create multiple endpoints, when we create new ones.
APIUtil.getPropsValue(s"dynamicEndpoint.cache.ttl.seconds", "32").toInt
APIUtil.getPropsValue(s"dynamicEndpoint.cache.ttl.seconds", "0").toInt
}
override def create(bankId:Option[String], userId: String, swaggerString: String): Box[DynamicEndpointT] = {
@ -50,7 +51,8 @@ object MappedDynamicEndpointProvider extends DynamicEndpointProvider with Custom
By(DynamicEndpoint.BankId, bankId.getOrElse(""))
)
).map(dynamicEndpoint => {
dynamicEndpoint.SwaggerString(json.compactRender(json.parse(dynamicEndpoint.swaggerString).replace("host" :: Nil, JString(hostString)))).saveMe()
val updatedHost = DynamicEndpointHelper.changeOpenApiVersionHost(dynamicEndpoint.swaggerString, hostString )
dynamicEndpoint.SwaggerString(updatedHost).saveMe()
}
)
}

View File

@ -30,7 +30,7 @@ trait EntitlementProvider {
def getEntitlementsByRole(roleName: String): Box[List[Entitlement]]
def getEntitlementsFuture() : Future[Box[List[Entitlement]]]
def getEntitlementsByRoleFuture(roleName: String) : Future[Box[List[Entitlement]]]
def addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String="manual") : Box[Entitlement]
def addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String="manual", grantorUserId: Option[String]=None) : Box[Entitlement]
def deleteDynamicEntityEntitlement(entityName: String, bankId:Option[String]) : Box[Boolean]
def deleteEntitlements(entityNames: List[String]) : Box[Boolean]
}
@ -54,7 +54,7 @@ class RemotedataEntitlementsCaseClasses {
case class getEntitlementsByRole(roleName: String)
case class getEntitlementsFuture()
case class getEntitlementsByRoleFuture(roleName: String)
case class addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String="manual")
case class addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String="manual", grantorUserId: Option[String]=None)
case class deleteDynamicEntityEntitlement(entityName: String, bankId:Option[String])
case class deleteEntitlements(entityNames: List[String])
}

View File

@ -1,12 +1,15 @@
package code.entitlement
import code.api.util.ApiRole.{CanCreateEntitlementAtAnyBank, CanCreateEntitlementAtOneBank}
import code.api.util.ErrorMessages
import code.api.v4_0_0.dynamic.DynamicEntityInfo
import code.util.{MappedUUID, UUIDString}
import net.liftweb.common.Box
import net.liftweb.common.{Box, Failure, Full}
import net.liftweb.mapper._
import scala.concurrent.Future
import com.openbankproject.commons.ExecutionContext.Implicits.global
import net.liftweb.common
object MappedEntitlementsProvider extends EntitlementProvider {
override def getEntitlement(bankId: String, userId: String, roleName: String): Box[MappedEntitlement] = {
@ -102,15 +105,26 @@ object MappedEntitlementsProvider extends EntitlementProvider {
}
}
override def addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String ="manual"): Box[Entitlement] = {
override def addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String ="manual", grantorUserId: Option[String]=None): Box[Entitlement] = {
def addEntitlementToUser(): Full[MappedEntitlement] = {
val addEntitlement: MappedEntitlement =
MappedEntitlement.create.mBankId(bankId).mUserId(userId).mRoleName(roleName).mCreatedByProcess(createdByProcess)
.saveMe()
Full(addEntitlement)
}
// Return a Box so we can handle errors later.
val addEntitlement = MappedEntitlement.create
.mBankId(bankId)
.mUserId(userId)
.mRoleName(roleName)
.mCreatedByProcess(createdByProcess)
.saveMe()
Some(addEntitlement)
grantorUserId match {
case Some(userId) =>
val canCreateEntitlementAtAnyBank = MappedEntitlement.findAll(By(MappedEntitlement.mUserId, userId)).exists(e => e.roleName == CanCreateEntitlementAtAnyBank)
val canCreateEntitlementAtOneBank = MappedEntitlement.findAll(By(MappedEntitlement.mUserId, userId)).exists(e => e.roleName == CanCreateEntitlementAtOneBank && e.bankId == bankId)
if(canCreateEntitlementAtAnyBank || canCreateEntitlementAtOneBank) {
addEntitlementToUser()
} else {
Failure(ErrorMessages.EntitlementCannotBeGrantedGrantorIssue)
}
case None =>
addEntitlementToUser()
}
}
}

View File

@ -935,7 +935,7 @@ def restoreSomeSessions(): Unit = {
// variable redirect is from loginRedirect, it is set-up in OAuthAuthorisation.scala as following code:
// val currentUrl = S.uriAndQueryString.getOrElse("/")
// AuthUser.loginRedirect.set(Full(Helpers.appendParams(currentUrl, List((LogUserOutParam, "false")))))
def checkInternalRedirectAndLogUseIn(preLoginState: () => Unit, redirect: String, user: AuthUser) = {
def checkInternalRedirectAndLogUserIn(preLoginState: () => Unit, redirect: String, user: AuthUser) = {
if (Helper.isValidInternalRedirectUrl(redirect)) {
logUserIn(user, () => {
S.notice(S.?("logged.in"))
@ -943,12 +943,12 @@ def restoreSomeSessions(): Unit = {
if(emailDomainToSpaceMappings.nonEmpty){
Future{
tryo{AuthUser.grantEntitlementsToUseDynamicEndpointsInSpaces(user)}
.openOr(logger.error(s"${user} checkInternalRedirectAndLogUseIn.grantEntitlementsToUseDynamicEndpointsInSpaces throw exception! "))
.openOr(logger.error(s"${user} checkInternalRedirectAndLogUserIn.grantEntitlementsToUseDynamicEndpointsInSpaces throw exception! "))
}}
if(emailDomainToEntitlementMappings.nonEmpty){
Future{
tryo{AuthUser.grantEmailDomainEntitlementsToUser(user)}
.openOr(logger.error(s"${user} checkInternalRedirectAndLogUseIn.grantEmailDomainEntitlementsToUser throw exception! "))
.openOr(logger.error(s"${user} checkInternalRedirectAndLogUserIn.grantEmailDomainEntitlementsToUser throw exception! "))
}}
S.redirectTo(redirect)
})
@ -996,9 +996,11 @@ def restoreSomeSessions(): Unit = {
// Reset any bad attempt
LoginAttempt.resetBadLoginAttempts(usernameFromGui)
val preLoginState = capturePreLoginState()
// User init actions
AfterApiAuth.innerLoginUserInitAction(Full(user))
logger.info("login redirect: " + loginRedirect.get)
val redirect = redirectUri()
checkInternalRedirectAndLogUseIn(preLoginState, redirect, user)
checkInternalRedirectAndLogUserIn(preLoginState, redirect, user)
} else { // If user is NOT locked AND password is wrong => increment bad login attempt counter.
LoginAttempt.incrementBadLoginAttempts(usernameFromGui)
S.error(Helper.i18n("invalid.login.credentials"))
@ -1021,7 +1023,9 @@ def restoreSomeSessions(): Unit = {
//This method is used for connector = kafka* || obpjvm*
//It will update the views and createAccountHolder ....
registeredUserHelper(user.username.get)
checkInternalRedirectAndLogUseIn(preLoginState, redirect, user)
// User init actions
AfterApiAuth.innerLoginUserInitAction(Full(user))
checkInternalRedirectAndLogUserIn(preLoginState, redirect, user)
// If user cannot be found locally, try to authenticate user via connector
case Empty if (APIUtil.getPropsAsBoolValue("connector.user.authentication", false) ||
@ -1034,7 +1038,9 @@ def restoreSomeSessions(): Unit = {
externalUserHelper(usernameFromGui, passwordFromGui) match {
case Full(user: AuthUser) =>
LoginAttempt.resetBadLoginAttempts(usernameFromGui)
checkInternalRedirectAndLogUseIn(preLoginState, redirect, user)
// User init actions
AfterApiAuth.innerLoginUserInitAction(Full(user))
checkInternalRedirectAndLogUserIn(preLoginState, redirect, user)
case _ =>
LoginAttempt.incrementBadLoginAttempts(username.get)
Empty

View File

@ -48,8 +48,8 @@ object RemotedataEntitlements extends ObpActorInit with EntitlementProvider {
def getEntitlementsByRoleFuture(roleName: String) : Future[Box[List[Entitlement]]] =
(actor ? cc.getEntitlementsByRoleFuture(roleName)).mapTo[Box[List[Entitlement]]]
def addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String="manual") : Box[Entitlement] = getValueFromFuture(
(actor ? cc.addEntitlement(bankId, userId, roleName, createdByProcess: String)).mapTo[Box[Entitlement]]
def addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String="manual", grantorUserId: Option[String]=None) : Box[Entitlement] = getValueFromFuture(
(actor ? cc.addEntitlement(bankId, userId, roleName, createdByProcess, grantorUserId)).mapTo[Box[Entitlement]]
)
override def deleteDynamicEntityEntitlement(entityName: String, bankId:Option[String]): Box[Boolean] = getValueFromFuture(

View File

@ -55,9 +55,9 @@ class RemotedataEntitlementsActor extends Actor with ObpActorHelper with MdcLogg
logger.debug(s"getEntitlementsByRole($role)")
sender ! (mapper.getEntitlementsByRole(role))
case cc.addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String) =>
logger.debug(s"addEntitlement($bankId, $userId, $roleName, $createdByProcess)")
sender ! (mapper.addEntitlement(bankId, userId, roleName, createdByProcess: String))
case cc.addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String, grantorUserId: Option[String]) =>
logger.debug(s"addEntitlement($bankId, $userId, $roleName, $createdByProcess, $grantorUserId)")
sender ! (mapper.addEntitlement(bankId, userId, roleName, createdByProcess, grantorUserId))
case cc.deleteDynamicEntityEntitlement(entityName: String, bankId:Option[String]) =>
logger.debug(s"deleteDynamicEntityEntitlement($entityName) bankId($bankId)")

View File

@ -29,6 +29,7 @@ package code.snippet
import java.time.{Duration, ZoneId, ZoneOffset, ZonedDateTime}
import java.util.Date
import code.api.Constant
import code.api.util.{APIUtil, SecureRandomUtil}
import code.model.dataAccess.{AuthUser, ResourceUser}
import code.users
@ -102,9 +103,10 @@ class UserInvitation extends MdcLoggable {
else if(termsCheckboxVar.is == false) showErrorsForTermsAndConditions()
else if(personalDataCollectionConsentCountryWaiverList.exists(_.toLowerCase == countryVar.is.toLowerCase) == false && consentForCollectingCheckboxVar.is == false) showErrorsForConsentForCollectingPersonalData()
else {
val localIdentityProviderUrl = APIUtil.getPropsValue("local_identity_provider_url", Constant.HostName)
// Resource User table
createResourceUser(
provider = "OBP-User-Invitation",
provider = localIdentityProviderUrl, // TODO Make provider an enum
providerId = Some(usernameVar.is),
name = Some(usernameVar.is),
email = Some(email),

View File

@ -0,0 +1,29 @@
package code.users
import code.util.MappedUUID
import net.liftweb.mapper._
class UserInitAction extends UserInitActionTrait with LongKeyedMapper[UserInitAction] with IdPK with CreatedUpdated {
def getSingleton = UserInitAction
object UserId extends MappedUUID(this)
object ActionName extends MappedString(this, 100)
object ActionValue extends MappedString(this, 100)
object Success extends MappedBoolean(this)
override def userId: String = UserId.get
override def actionName: String = ActionName.get
override def actionValue: String = ActionValue.get
override def success: Boolean = Success.get
}
object UserInitAction extends UserInitAction with LongKeyedMetaMapper[UserInitAction] {
override def dbIndexes: List[BaseIndex[UserInitAction]] = UniqueIndex(UserId, ActionName, ActionValue) :: super.dbIndexes
}
trait UserInitActionTrait {
def userId: String
def actionName: String
def actionValue: String
def success: Boolean
}

View File

@ -0,0 +1,28 @@
package code.users
import cats.Now
import code.util.Helper.MdcLoggable
import net.liftweb.common.{Box, Full}
import net.liftweb.mapper.By
import net.liftweb.util.Helpers
object UserInitActionProvider extends MdcLoggable {
def createOrUpdateInitAction(userId: String, actionName: String, actionValue: String, success: Boolean): Box[UserInitAction] = {
UserInitAction.find(
By(UserInitAction.UserId, userId),
By(UserInitAction.ActionName, actionName),
By(UserInitAction.ActionValue, actionValue)
) match {
case Full(action) => Some(action.Success(success).updatedAt(Helpers.now).saveMe())
case _ =>
Some(
UserInitAction.create
.UserId(userId)
.ActionName(actionName)
.ActionValue(actionValue)
.Success(success)
.saveMe()
)
}
}
}

View File

@ -0,0 +1,38 @@
package deletion
import code.api.APIFailureNewStyle
import code.api.util.APIUtil.fullBoxOrException
import code.api.util.ErrorMessages.CouldNotDeleteCascade
import code.model.dataAccess.{MappedBank, MappedBankAccount}
import com.openbankproject.commons.model.{AccountId, BankId}
import deletion.DeletionUtil.databaseAtomicTask
import net.liftweb.common.{Box, Empty, Full}
import net.liftweb.db.DB
import net.liftweb.mapper.By
import net.liftweb.util.DefaultConnectionIdentifier
object DeleteBankCascade {
def delete(bankId: BankId): Boolean = {
MappedBankAccount.findAll(By(MappedBankAccount.bank, bankId.value))
.forall(i => DeleteAccountCascade.delete(i.bankId, i.accountId)) && deleteBank(bankId)
}
def atomicDelete(bankId: BankId): Box[Boolean] = databaseAtomicTask {
delete(bankId) match {
case true =>
Full(true)
case false =>
DB.rollback(DefaultConnectionIdentifier)
fullBoxOrException(Empty ~> APIFailureNewStyle(CouldNotDeleteCascade, 400))
}
}
private def deleteBank(bankId: BankId): Boolean = {
MappedBank.bulkDelete_!!(
By(MappedBank.permalink, bankId.value)
)
}
}

View File

@ -0,0 +1,110 @@
package deletion
import code.accountapplication.MappedAccountApplication
import code.api.APIFailureNewStyle
import code.api.util.APIUtil.fullBoxOrException
import code.api.util.ErrorMessages.CouldNotDeleteCascade
import code.customer.MappedCustomer
import code.customer.internalMapping.MappedCustomerIdMapping
import code.customeraddress.MappedCustomerAddress
import code.customerattribute.MappedCustomerAttribute
import code.kycchecks.MappedKycCheck
import code.kycdocuments.MappedKycDocument
import code.kycmedias.MappedKycMedia
import code.kycstatuses.MappedKycStatus
import code.taxresidence.MappedTaxResidence
import code.usercustomerlinks.MappedUserCustomerLink
import com.openbankproject.commons.model.CustomerId
import deletion.DeletionUtil.databaseAtomicTask
import net.liftweb.common.{Box, Empty, Full}
import net.liftweb.db.DB
import net.liftweb.mapper.By
import net.liftweb.util.DefaultConnectionIdentifier
object DeleteCustomerCascade {
def delete(customerId: CustomerId): Boolean = {
val doneTasks =
deleteCustomerAttributes(customerId) ::
deleteTaxResidence(customerId) ::
deleteKycStatus(customerId) ::
deleteKycMedia(customerId) ::
deleteKycCheck(customerId) ::
deleteKycDocument(customerId) ::
deleteCustomerAddress(customerId) ::
deleteCustomerIdMapping(customerId) ::
deleteAccountApplication(customerId) ::
deleteCustomerUserCustomerLinks(customerId) ::
deleteCustomer(customerId) ::
Nil
doneTasks.forall(_ == true)
}
def atomicDelete(customerId: CustomerId): Box[Boolean] = databaseAtomicTask {
delete(customerId) match {
case true =>
Full(true)
case false =>
DB.rollback(DefaultConnectionIdentifier)
fullBoxOrException(Empty ~> APIFailureNewStyle(CouldNotDeleteCascade, 400))
}
}
private def deleteCustomerAttributes(customerId: CustomerId): Boolean = {
MappedCustomerAttribute.bulkDelete_!!(By(MappedCustomerAttribute.mCustomerId, customerId.value))
}
private def deleteCustomer(customerId: CustomerId): Boolean = {
MappedCustomer.bulkDelete_!!(
By(MappedCustomer.mCustomerId, customerId.value)
)
}
private def deleteCustomerUserCustomerLinks(customerId: CustomerId): Boolean = {
MappedUserCustomerLink.bulkDelete_!!(
By(MappedUserCustomerLink.mCustomerId, customerId.value)
)
}
private def deleteTaxResidence(customerId: CustomerId): Boolean = {
MappedCustomer.find(By(MappedCustomer.mCustomerId, customerId.value)).forall(c =>
MappedTaxResidence.bulkDelete_!!(
By(MappedTaxResidence.mCustomerId, c.id.get)
))
}
private def deleteKycStatus(customerId: CustomerId): Boolean = {
MappedKycStatus.bulkDelete_!!(
By(MappedKycStatus.mCustomerId, customerId.value)
)
}
private def deleteKycMedia(customerId: CustomerId): Boolean = {
MappedKycMedia.bulkDelete_!!(
By(MappedKycMedia.mCustomerId, customerId.value)
)
}
private def deleteKycCheck(customerId: CustomerId): Boolean = {
MappedKycCheck.bulkDelete_!!(
By(MappedKycCheck.mCustomerId, customerId.value)
)
}
private def deleteKycDocument(customerId: CustomerId): Boolean = {
MappedKycDocument.bulkDelete_!!(
By(MappedKycDocument.mCustomerId, customerId.value)
)
}
private def deleteCustomerAddress(customerId: CustomerId): Boolean = {
MappedCustomer.find(By(MappedCustomer.mCustomerId, customerId.value)).forall(c =>
MappedCustomerAddress.bulkDelete_!!(
By(MappedCustomerAddress.mCustomerId, c.id.get)
))
}
private def deleteAccountApplication(customerId: CustomerId): Boolean = {
MappedAccountApplication.bulkDelete_!!(
By(MappedAccountApplication.mCustomerId, customerId.value)
)
}
private def deleteCustomerIdMapping(customerId: CustomerId): Boolean = {
MappedCustomerIdMapping.bulkDelete_!!(
By(MappedCustomerIdMapping.mCustomerId, customerId.value)
)
}
}

View File

@ -20,8 +20,7 @@ nav .navbar-collapse.collapse {
margin-left: 0;
}
.navbar-default .navbar-nav > li #navitem-logo img{
width:80px;
height: 40px;
height: 100%;
}
.navbar-default .navbar-nav > li > a {
background-color: white;

View File

@ -108,7 +108,7 @@ class DirectLoginTest extends ServerSetup with BeforeAndAfter {
val invalidConsumerKeyHeaders = List(accessControlOriginHeader, invalidConsumerKeyHeader)
val validHeaders = List(accessControlOriginHeader, validHeader)
val validHeaders = List(accessControlOriginHeader, validHeader, ("Authorization", "Basic 123456"))
val validDeprecatedHeaders = List(accessControlOriginHeader, validDeprecatedHeader)
val disabledConsumerKeyHeaders = List(accessControlOriginHeader, disabledConsumerValidHeader)

View File

@ -0,0 +1,112 @@
package code.api.v4_0_0
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.createViewJson
import code.api.util.APIUtil.OAuth._
import code.api.util.{APIUtil, ApiRole}
import code.api.util.ApiRole.{CanDeleteAccountCascade, CanDeleteBankCascade}
import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn}
import code.api.v3_1_0.CreateAccountResponseJsonV310
import code.api.v4_0_0.OBPAPI4_0_0.Implementations4_0_0
import code.entitlement.Entitlement
import code.model.dataAccess.MappedBank
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model.{AmountOfMoneyJsonV121, ErrorMessage}
import com.openbankproject.commons.util.ApiVersion
import net.liftweb.json.Serialization.write
import net.liftweb.mapper.By
import org.scalatest.Tag
class DeleteBankCascadeTest extends V400ServerSetup {
/**
* 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.v4_0_0.toString)
object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.deleteBankCascade))
lazy val addAccountJson = SwaggerDefinitionsJSON.createAccountRequestJsonV310.copy(user_id = resourceUser1.userId, balance = AmountOfMoneyJsonV121("EUR","0"))
feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") {
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
val bankId = createBank(APIUtil.generateUUID()).bankId.value
When("We make a request v4.0.0")
val request400 = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId ).DELETE
val response400 = makeDeleteRequest(request400)
Then("We should get a 401")
response400.code should equal(401)
response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
}
feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access") {
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
When("We make a request v4.0.0")
val bankId = createBank(APIUtil.generateUUID()).bankId.value
val request400 = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId ).DELETE <@(user1)
val response400 = makeDeleteRequest(request400)
Then("We should get a 403")
response400.code should equal(403)
val errorMessage = response400.body.extract[ErrorMessage].message
errorMessage contains (UserHasMissingRoles) should be (true)
errorMessage contains (CanDeleteBankCascade.toString()) should be (true)
}
}
feature(s"test $ApiEndpoint1 - Authorized access") {
scenario("We will call the endpoint with user credentials", ApiEndpoint1, VersionOfApi) {
When("We grant the role")
val bankId = createBank(APIUtil.generateUUID()).bankId.value
Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, ApiRole.canCreateAccount.toString)
And("We make a request v4.0.0")
val request400 = (v4_0_0_Request / "banks" / bankId / "accounts" ).POST <@(user1)
val response400 = makePostRequest(request400, write(addAccountJson))
Then("We should get a 201")
response400.code should equal(201)
val account = response400.body.extract[CreateAccountResponseJsonV310]
account.account_id should not be empty
val postBodyView = createViewJson.copy(name = "_cascade_delete", metadata_view = "_cascade_delete", is_public = false)
createViewViaEndpoint(bankId, account.account_id, postBodyView, user1)
createAccountAttributeViaEndpoint(
bankId,
account.account_id,
"REQUIRED_CHALLENGE_ANSWERS",
"2",
"INTEGER"
)
grantUserAccessToViewViaEndpoint(
bankId,
account.account_id,
resourceUser2.userId,
user1,
PostViewJsonV400(view_id = "owner", is_system = true)
)
createWebhookViaEndpoint(
bankId,
account.account_id,
resourceUser1.userId,
user1
)
When("We grant the role")
Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, ApiRole.canDeleteBankCascade.toString)
And("We make a delete cascade request v4.0.0")
val deleteRequest400 = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId ).DELETE <@(user1)
val deleteResponse400 = makeDeleteRequest(deleteRequest400)
Then("We should get a 200")
deleteResponse400.code should equal(200)
When("We try to delete one more time")
makeDeleteRequest(request400).code should equal(404)
}
}
}

View File

@ -0,0 +1,74 @@
package code.api.v4_0_0
import code.api.util.APIUtil.OAuth._
import code.api.util.ApiRole
import code.api.util.ApiRole.{CanDeleteCustomerCascade, CanDeleteTransactionCascade}
import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn}
import code.api.v4_0_0.OBPAPI4_0_0.Implementations4_0_0
import code.entitlement.Entitlement
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.ApiVersion
import org.scalatest.Tag
class DeleteCustomerCascadeTest extends V400ServerSetup {
/**
* 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.v4_0_0.toString)
object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.deleteCustomerCascade))
lazy val bankId = randomBankId
lazy val bankAccount = randomPrivateAccountViaEndpoint(bankId)
feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") {
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
When("We make a request v4.0.0")
val request400 = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId /
"customers" / "CUSTOMER_ID" ).DELETE
val response400 = makeDeleteRequest(request400)
Then("We should get a 401")
response400.code should equal(401)
response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
}
feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access") {
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
When("We make a request v4.0.0")
val request400 = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId /
"customers" / "CUSTOMER_ID" ).DELETE <@(user1)
val response400 = makeDeleteRequest(request400)
Then("We should get a 403")
response400.code should equal(403)
val errorMessage = response400.body.extract[ErrorMessage].message
errorMessage contains (UserHasMissingRoles) should be (true)
errorMessage contains (CanDeleteCustomerCascade.toString()) should be (true)
}
}
feature(s"test $ApiEndpoint1 - Authorized access") {
scenario("We will call the endpoint with user credentials", ApiEndpoint1, VersionOfApi) {
val customerId = createAndGetCustomerIdViaEndpoint(bankId, user1)
Then("we create the Customer Attribute")
createAndGetCustomerAttributeIdViaEndpoint(bankId:String, customerId:String, user1)
When("We make a request v4.0.0")
Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, ApiRole.canDeleteCustomerCascade.toString)
val request400 = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId /
"customers" / customerId ).DELETE <@(user1)
val response400 = makeDeleteRequest(request400)
Then("We should get a 200")
response400.code should equal(200)
When("We try to delete one more time we should get 404")
makeDeleteRequest(request400).code should equal(404)
}
}
}

View File

@ -25,28 +25,16 @@ class DynamicIntegrationTest extends V400ServerSetup {
object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString)
object DynamicIntegration extends Tag("Dynamic Entity/Dynamic/Mapping")
object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.createBankLevelEndpointMapping))
object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getBankLevelEndpointMapping))
object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getAllBankLevelEndpointMappings))
object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.updateBankLevelEndpointMapping))
object ApiEndpoint5 extends Tag(nameOf(Implementations4_0_0.deleteBankLevelEndpointMapping))
object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.createBankLevelDynamicEndpoint))
object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.createBankLevelDynamicEntity))
object ApiEndpoint6 extends Tag(nameOf(Implementations4_0_0.createBankLevelDynamicEndpoint))
object ApiEndpoint7 extends Tag(nameOf(Implementations4_0_0.getBankLevelDynamicEndpoints))
object ApiEndpoint8 extends Tag(nameOf(Implementations4_0_0.getBankLevelDynamicEndpoint))
object ApiEndpoint9 extends Tag(nameOf(Implementations4_0_0.deleteBankLevelDynamicEndpoint))
object ApiEndpoint10 extends Tag(nameOf(Implementations4_0_0.getBankLevelDynamicEntities))
object ApiEndpoint11 extends Tag(nameOf(Implementations4_0_0.createBankLevelDynamicEntity))
object ApiEndpoint12 extends Tag(nameOf(Implementations4_0_0.getBankLevelDynamicEntities))
object ApiEndpoint13 extends Tag(nameOf(Implementations4_0_0.deleteBankLevelDynamicEntity))
object ApiEndpoint14 extends Tag(nameOf(Implementations4_0_0.updateBankLevelDynamicEntity))
val mapping = endpointMappingRequestBodyExample
val dynamicEntity = dynamicEntityRequestBodyExample.copy(bankId = None)
val dynamicEndpoint = dynamicEndpointRequestBodyExample
feature("test Dynamic Entity/Endpoint and endpoint mappings together") {
feature(s"test Dynamic Entity/Endpoint and endpoint mappings together $ApiEndpoint1 $ApiEndpoint2 $ApiEndpoint3") {
scenario("test Dynamic Entity/Endpoint and endpoint mappings together ", DynamicIntegration, VersionOfApi) {
//First, we need to prepare the dynamic entity, it should have two fields: name, balance.
Entitlement.entitlement.vend.addEntitlement(testBankId1.value, resourceUser1.userId, CanCreateBankLevelDynamicEntity.toString)