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 }