libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

EbicsSetup.kt (8714B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2023-2025 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.nexus.cli
     21 
     22 import com.github.ajalt.clikt.core.CliktCommand
     23 import com.github.ajalt.clikt.core.Context
     24 import com.github.ajalt.clikt.parameters.groups.provideDelegate
     25 import com.github.ajalt.clikt.parameters.options.flag
     26 import com.github.ajalt.clikt.parameters.options.option
     27 import io.ktor.client.*
     28 import tech.libeufin.common.*
     29 import tech.libeufin.common.crypto.CryptoUtil
     30 import tech.libeufin.ebics.ClientPrivateKeysFile
     31 import tech.libeufin.ebics.BankPublicKeysFile
     32 import tech.libeufin.ebics.ebicsSetup
     33 import tech.libeufin.ebics.askUserToAcceptKeys
     34 import tech.libeufin.ebics.EbicsLogger
     35 import tech.libeufin.ebics.EbicsKeyMng
     36 import tech.libeufin.nexus.*
     37 import tech.libeufin.ebics.*
     38 import java.nio.file.FileAlreadyExistsException
     39 import java.nio.file.Path
     40 import java.nio.file.StandardOpenOption
     41 import java.time.Instant
     42 import kotlin.io.path.Path
     43 import kotlin.io.path.writeBytes
     44 
     45 fun expectFullKeys(cfg: EbicsKeysConfig): Pair<ClientPrivateKeysFile, BankPublicKeysFile> =
     46     expectFullKeys(cfg, "libeufin-nexus ebics-setup")
     47 
     48 /**
     49  * CLI class implementing the "ebics-setup" subcommand.
     50  */
     51 class EbicsSetup: TalerCmd() {
     52     override fun help(context: Context) = "Set up the EBICS subscriber"
     53 
     54     private val forceKeysResubmission by option(
     55         help = "Resubmits all the keys to the bank"
     56     ).flag(default = false)
     57     private val autoAcceptKeys by option(
     58         help = "Accepts the bank keys without interactively asking the user"
     59     ).flag(default = false)
     60     private val generateRegistrationPdf by option(
     61         help = "Generates the PDF with the client public keys to send to the bank"
     62     ).flag(default = false)
     63     private val ebicsLog by ebicsLogOption()
     64     /**
     65      * This function collects the main steps of setting up an EBICS access.
     66      */
     67     override fun run() = cliCmd(logger) {
     68         val cfg = nexusConfig(config)
     69         val setupCfg = cfg.setup
     70 
     71         val client = httpClient()
     72         val ebicsLogger = EbicsLogger(ebicsLog)
     73 
     74         val ebics3 = when (cfg.ebics.dialect) {
     75             // TODO GLS needs EBICS 2.5 for key management
     76             Dialect.gls -> false
     77             else -> true
     78         }
     79         val (clientKeys, bankKeys) = ebicsSetup(
     80             client,
     81             ebicsLogger,
     82             cfg.ebics,
     83             cfg.ebics.host,
     84             cfg.setup,
     85             forceKeysResubmission,
     86             generateRegistrationPdf,
     87             autoAcceptKeys,
     88             ebics3
     89         )
     90         
     91         // Check account information
     92         logger.info("Doing administrative request HKD")
     93         cfg.withDb { db, _ ->
     94             EbicsClient(
     95                 cfg.ebics.host,
     96                 client, 
     97                 db.ebics,
     98                 ebicsLogger,
     99                 clientKeys,
    100                 bankKeys
    101             ).download(EbicsOrder.V3.HKD) { stream ->
    102                 val (partner, users) = EbicsAdministrative.parseHKD(stream)
    103                 val user = users.find { it -> it.id == cfg.ebics.host.userId }
    104                 // Debug logging
    105                 logger.debug {
    106                     buildString {
    107                         if (partner.name != null || partner.accounts.isNotEmpty()) {
    108                             append("Partner Info: ")
    109                             if (partner.name != null) {
    110                                 append("'")
    111                                 append(partner.name)
    112                                 append("'")
    113                             }
    114                             for ((currency, iban, bic) in partner.accounts) {
    115                                 append(' ')
    116                                 append(currency)
    117                                 append('-')
    118                                 append(iban)
    119                                 append('-')
    120                                 append(bic)
    121                             }
    122                             append('\n')
    123                         }
    124                         append("Supported orders:\n")
    125                         for ((order, description) in partner.orders) {
    126                             append("- ")
    127                             append(order.description())
    128                             append(": ")
    129                             append(description)
    130                             append('\n')
    131                         }
    132                         if (user != null) {
    133                             append("Authorized orders:\n")
    134                             for ((order) in partner.orders) {
    135                                 append("- ")
    136                                 append(order.description())
    137                                 append('\n')
    138                             }
    139                         }
    140                     }
    141                 }
    142 
    143                 // Check partner info match config
    144                 if (partner.name != null && partner.name != cfg.ebics.account.name)
    145                     logger.warn("Expected NAME '${cfg.ebics.account.name}' from config got '${partner.name}' from bank")
    146                 val account = partner.accounts.find { it.iban == cfg.ebics.account.iban.toString() }
    147                 if (account != null) {
    148                     if (account.currency != null && account.currency != cfg.currency)
    149                         logger.error("Expected CURRENCY '${cfg.currency}' from config got '${account.currency}' from bank")
    150                     if (account.bic != cfg.ebics.account.bic)
    151                         logger.error("Expected BIC '${cfg.ebics.account.bic}' from config got '${account.bic}' from bank")
    152                 } else if (partner.accounts.isNotEmpty()) {
    153                     val ibans = partner.accounts.map { it.iban }.joinToString(" ")
    154                     logger.error("Expected IBAN ${cfg.ebics.account.iban} from config got $ibans from bank")
    155                 }
    156 
    157                 val instantDebitOrder = cfg.ebics.dialect.instantDirectDebit()
    158                 val debitOrder = cfg.ebics.dialect.directDebit()
    159                 val requireOrders = cfg.ebics.dialect.downloadOrders()
    160 
    161                 val partnerOrders = partner.orders.map { it.order }
    162 
    163                 // Check partner support for direct debit orders
    164                 if (instantDebitOrder != null && instantDebitOrder !in partnerOrders) {
    165                     logger.warn("Unsupported instant debit order: ${instantDebitOrder.description()}")
    166                 }
    167                 if (debitOrder !in partnerOrders) {
    168                     logger.warn("Unsupported debit order: ${debitOrder.description()}")
    169                 }
    170                 
    171                 // Check partner support required orders
    172                 val unsupportedOrder = requireOrders subtract partnerOrders
    173                 if (unsupportedOrder.isNotEmpty()) {
    174                     logger.warn("Unsupported orders: {}", unsupportedOrder.map(EbicsOrder::description).joinToString(" "))
    175                 }
    176 
    177                 if (user != null) {
    178                     // Check user is authorized for direct debit orders
    179                     if (instantDebitOrder != null && instantDebitOrder in partnerOrders && instantDebitOrder !in user.permissions) {
    180                         logger.warn("Unauthorized instant debit order: ${instantDebitOrder.description()}")
    181                     }
    182                     if (debitOrder in partnerOrders && debitOrder !in user.permissions) {
    183                         logger.warn("Unauthorized debit order: ${debitOrder.description()}")
    184                     }
    185 
    186                     // Check user is authorized for required orders
    187                     val unauthorizedOrders = requireOrders subtract user.permissions subtract unsupportedOrder
    188                     if (unauthorizedOrders.isNotEmpty()) {
    189                         logger.warn("Unauthorized orders: {}", unauthorizedOrders.map(EbicsOrder::description).joinToString(" "))
    190                     }
    191 
    192                     logger.info("Subscriber status: {}", user.status.description)
    193                 }
    194             }
    195         }
    196         
    197         println("setup ready")
    198     }
    199 }