libeufin

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

TokenDAO.kt (8294B)


      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.bank.db
     21 
     22 import tech.libeufin.bank.*
     23 import tech.libeufin.common.PageParams
     24 import tech.libeufin.common.asInstant
     25 import tech.libeufin.common.db.*
     26 import tech.libeufin.common.micros
     27 import java.time.Instant
     28 
     29 /** Data access logic for auth tokens */
     30 class TokenDAO(private val db: Database) {
     31     /** Result status of token creation */
     32     sealed interface TokenCreationResult {
     33         data object Success: TokenCreationResult
     34         data object TanRequired: TokenCreationResult
     35     }
     36 
     37     /** Create new token for [username] */
     38     suspend fun create(
     39         username: String,
     40         content: ByteArray,
     41         creationTime: Instant,
     42         expirationTime: Instant,
     43         scope: TokenScope,
     44         isRefreshable: Boolean,
     45         description: String?,
     46         is2fa: Boolean
     47     ): TokenCreationResult = db.serializable(
     48         """
     49         SELECT out_tan_required FROM create_token(
     50             ?,?,?,?,?::token_scope_enum,?,?,?
     51         )
     52         """
     53     ) {
     54         bind(username)
     55         bind(content)
     56         bind(creationTime)
     57         bind(expirationTime)
     58         bind(scope)
     59         bind(isRefreshable)
     60         bind(description)
     61         bind(is2fa)
     62         one {
     63             when {
     64                 it.getBoolean("out_tan_required") -> TokenCreationResult.TanRequired
     65                 else -> TokenCreationResult.Success
     66             }
     67         }
     68     }
     69 
     70     /**
     71      * Atomically create a replacement for [source] and shorten the source
     72      * token's lifetime to [sourceExpirationCap].  LEAST makes the overlap
     73      * deadline non-sliding when the same source is refreshed more than once.
     74      */
     75     suspend fun refresh(
     76         username: String,
     77         source: ByteArray,
     78         content: ByteArray,
     79         creationTime: Instant,
     80         expirationTime: Instant,
     81         sourceExpirationCap: Instant,
     82         scope: TokenScope,
     83         isRefreshable: Boolean,
     84         description: String?
     85     ): Boolean = db.serializable(
     86         """
     87         WITH source_token AS (
     88           UPDATE bearer_tokens
     89              SET expiration_time=LEAST(expiration_time, ?)
     90            WHERE content=?
     91              AND bank_customer=(
     92                SELECT customer_id FROM customers
     93                 WHERE username=? AND deleted_at IS NULL
     94              )
     95           RETURNING bank_customer
     96         ), replacement AS (
     97           INSERT INTO bearer_tokens (
     98             content, creation_time, expiration_time, scope, bank_customer,
     99             is_refreshable, description, last_access
    100           )
    101           SELECT ?, ?, ?, ?::token_scope_enum, bank_customer, ?, ?, ?
    102             FROM source_token
    103           RETURNING bearer_token_id
    104         )
    105         SELECT EXISTS(SELECT FROM replacement)
    106         """
    107     ) {
    108         bind(sourceExpirationCap)
    109         bind(source)
    110         bind(username)
    111         bind(content)
    112         bind(creationTime)
    113         bind(expirationTime)
    114         bind(scope)
    115         bind(isRefreshable)
    116         bind(description)
    117         bind(creationTime)
    118         one { it.getBoolean(1) }
    119     }
    120     
    121     /** Get info for [token] */
    122     suspend fun access(token: ByteArray, accessTime: Instant): BearerToken? = db.serializable(
    123         """
    124         UPDATE bearer_tokens
    125             SET last_access=?
    126         FROM customers
    127         WHERE bank_customer=customer_id AND content=? AND deleted_at IS NULL
    128         RETURNING
    129             creation_time,
    130             expiration_time,
    131             scope,
    132             is_refreshable
    133         """
    134     ) {
    135         bind(accessTime)
    136         bind(token) 
    137         oneOrNull {
    138             BearerToken(
    139                 creationTime = it.getLong("creation_time").asInstant(),
    140                 expirationTime = it.getLong("expiration_time").asInstant(),
    141                 scope = it.getEnum("scope"),
    142                 isRefreshable = it.getBoolean("is_refreshable")
    143             )
    144         }
    145     }
    146 
    147     /** Get info for [token] and its associated bank account*/
    148     suspend fun accessInfo(token: ByteArray, accessTime: Instant): Pair<BearerToken, BankInfo>? = db.serializable(
    149         """
    150         UPDATE bearer_tokens
    151             SET last_access=?
    152         FROM customers
    153             JOIN bank_accounts ON customer_id=owning_customer_id
    154         WHERE bank_customer=customer_id AND content=? AND deleted_at IS NULL
    155         RETURNING
    156             creation_time,
    157             expiration_time,
    158             scope,
    159             is_refreshable,
    160             username,
    161             is_taler_exchange,
    162             bank_account_id,
    163             internal_payto,
    164             name,
    165             tan_channels,
    166             email,
    167             phone
    168         """
    169     ) {
    170         bind(accessTime)
    171         bind(token) 
    172         oneOrNull {
    173             Pair(
    174                 BearerToken(
    175                     creationTime = it.getLong("creation_time").asInstant(),
    176                     expirationTime = it.getLong("expiration_time").asInstant(),
    177                     scope = it.getEnum("scope"),
    178                     isRefreshable = it.getBoolean("is_refreshable")
    179                 ),
    180                 BankInfo(
    181                     username = it.getString("username"),
    182                     payto = it.getBankPayto("internal_payto", "name", db.ctx),
    183                     bankAccountId = it.getLong("bank_account_id"),
    184                     isTalerExchange = it.getBoolean("is_taler_exchange"),
    185                     channels = it.getEnumSet<TanChannel>("tan_channels"),
    186                     phone = it.getString("phone"),
    187                     email = it.getString("email")
    188                 )
    189             )
    190         }
    191     }
    192     
    193     /** Delete token [token] */
    194     suspend fun delete(token: ByteArray) = db.serializable(
    195         "DELETE FROM bearer_tokens WHERE content = ?"
    196     ) {
    197         bind(token)
    198         executeUpdate()
    199     }
    200 
    201     /** Delete token [id] owned by the account identified by [username]. */
    202     suspend fun deleteById(id: Long, username: String) = db.serializable(
    203         """
    204         DELETE FROM bearer_tokens
    205          WHERE bearer_token_id = ?
    206            AND bank_customer=(
    207              SELECT customer_id FROM customers
    208               WHERE username=? AND deleted_at IS NULL
    209            )
    210         """
    211     ) {
    212         bind(id)
    213         bind(username)
    214         executeUpdateCheck()
    215     }
    216 
    217     /** Get a page of all tokens of [username] accounts */
    218     suspend fun page(params: PageParams, username: String, timestamp: Instant): List<TokenInfo>
    219         = db.page(
    220             params,
    221             "bearer_token_id",
    222             """
    223             SELECT
    224               creation_time,
    225               expiration_time,
    226               scope,
    227               is_refreshable,
    228               description,
    229               last_access,
    230               bearer_token_id
    231               FROM bearer_tokens 
    232               WHERE 
    233                 expiration_time > ? AND
    234                 bank_customer=(SELECT customer_id FROM customers WHERE deleted_at IS NULL AND username = ?) 
    235             AND
    236             """,
    237             {
    238                 bind(timestamp.micros())
    239                 bind(username)
    240             }
    241         ) {
    242             TokenInfo(
    243                 creation_time = it.getTalerTimestamp("creation_time"),
    244                 expiration = it.getTalerTimestamp("expiration_time"),
    245                 scope = it.getEnum("scope"),
    246                 refreshable = it.getBoolean("is_refreshable"),
    247                 description = it.getString("description"),
    248                 last_access = it.getTalerTimestamp("last_access"),
    249                 row_id = it.getLong("bearer_token_id"),
    250                 token_id = it.getLong("bearer_token_id")
    251             )
    252         }
    253 }