merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 7f4dd8a370a4917d2a5a8baf2542fec23c9102ef
parent 5cb9ea47557c18166a1f42768e495716c2f4594b
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Mon, 23 Jun 2025 11:45:13 +0200

Merge branch 'master' into dev/bohdan-potuzhnyi/donau-integration

Diffstat:
Mconfigure.ac | 4++--
Msrc/backend/taler-merchant-httpd.c | 727++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/backend/taler-merchant-httpd.h | 71++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/backend/taler-merchant-httpd_config.c | 2+-
Msrc/backend/taler-merchant-httpd_helper.c | 97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/backend/taler-merchant-httpd_helper.h | 12++++++++++++
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 2+-
Msrc/backend/taler-merchant-httpd_private-post-instances-ID-auth.c | 53+++++++++++++++++++++++++----------------------------
Msrc/backend/taler-merchant-httpd_private-post-instances-ID-token.c | 36+++++++++++++++++++++++-------------
Msrc/backend/taler-merchant-httpd_private-post-instances.c | 21+++++++--------------
Msrc/backend/taler-merchant-httpd_spa.c | 313++++---------------------------------------------------------------------------
Msrc/backenddb/pg_lookup_instances.c | 298+++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/include/taler_merchant_service.h | 106++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/include/taler_merchant_testing_lib.h | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler_merchantdb_plugin.h | 38--------------------------------------
Msrc/lib/Makefile.am | 2++
Asrc/lib/merchant_api_delete_instance_token.c | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/merchant_api_get_config.c | 4++--
Msrc/lib/merchant_api_post_instance_auth.c | 19++++---------------
Asrc/lib/merchant_api_post_instance_token.c | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/merchant_api_post_instances.c | 17++++-------------
Msrc/merchant-tools/taler-merchant-passwd.c | 24+++---------------------
Msrc/testing/Makefile.am | 1+
Msrc/testing/test_merchant_accounts.sh | 2+-
Msrc/testing/test_merchant_api.c | 42+++++++++++++++++++++++++++++++++---------
Msrc/testing/test_merchant_instance_auth.sh | 135++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/testing/test_merchant_instance_creation.sh | 4++--
Msrc/testing/test_merchant_instance_purge.sh | 3++-
Msrc/testing/test_merchant_instance_response.sh | 63++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/testing/test_merchant_product_creation.sh | 2--
Asrc/testing/testing_api_cmd_instance_token.c | 394+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/testing_api_cmd_pay_order.c | 2+-
32 files changed, 2080 insertions(+), 878 deletions(-)

diff --git a/configure.ac b/configure.ac @@ -253,12 +253,12 @@ AS_CASE([$with_exchange], CPPFLAGS="-I$with_exchange/include $CPPFLAGS $POSTGRESQL_CPPFLAGS"]) AC_CHECK_HEADERS([taler/taler_util.h], - [AC_CHECK_LIB([talerutil], [TALER_kyc_measure_authorization_hash], libtalerutil=1)]) + [AC_CHECK_LIB([talerutil], [TALER_merchant_instance_auth_hash_with_salt], libtalerutil=1)]) AM_CONDITIONAL(HAVE_TALERUTIL, test x$libtalerutil = x1) AS_IF([test $libtalerutil != 1], [AC_MSG_ERROR([[ *** -*** You need libtalerutil >= 0.13.0 to build this program. +*** You need libtalerutil >= 1.1.0 (ABI v8) to build this program. *** This library is part of the GNU Taler exchange, available at *** https://taler.net *** ]])]) diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014-2024 Taler Systems SA + (C) 2014-2025 Taler Systems SA TALER 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 @@ -33,6 +33,7 @@ #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_get-orders-ID.h" #include "taler-merchant-httpd_get-templates-ID.h" +#include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_private-delete-account-ID.h" #include "taler-merchant-httpd_private-delete-categories-ID.h" @@ -157,6 +158,12 @@ static struct GNUNET_DB_EventHandler *instance_eh; struct GNUNET_CONTAINER_MultiHashMap *TMH_by_id_map; /** + * #GNUNET_YES if protocol version 19 is strictly enforced. + * (Default is #GNUNET_NO) + */ +int TMH_strict_v19; + +/** * How long do we need to keep information on paid contracts on file for tax * or other legal reasons? Used to block deletions for younger transaction * data. @@ -205,118 +212,349 @@ static int global_ret; static const struct GNUNET_CONFIGURATION_Handle *cfg; /** - * Initial authorization token. + * Maximum length of a permissions string of a scope */ -char *TMH_default_auth; +#define TMH_MAX_SCOPE_PERMISSIONS_LEN 4096 +/** + * Maximum length of a name of a scope + */ +#define TMH_MAX_NAME_LEN 255 /** - * Check validity of login @a token for the given @a instance_id. - * - * @param token the login token given in the request - * @param instance_id the instance the login is to be checked against - * @param[out] as set to scope of the token if it is valid - * @return TALER_EC_NONE on success + * Represents a hard-coded set of default scopes with their + * permissions and names */ -static enum TALER_ErrorCode -TMH_check_token (const char *token, - const char *instance_id, - enum TMH_AuthScope *as) +struct ScopePermissionMap { - enum TMH_AuthScope scope; - struct GNUNET_TIME_Timestamp expiration; - enum GNUNET_DB_QueryStatus qs; - struct TALER_MERCHANTDB_LoginTokenP btoken; + /** + * The scope enum value + */ + enum TMH_AuthScope as; + + /** + * The scope name + */ + char name[TMH_MAX_NAME_LEN]; + + /** + * The scope permissions string. + * Comma-separated. + */ + char permissions[TMH_MAX_SCOPE_PERMISSIONS_LEN]; +}; - if (NULL == token) +/** + * The default scopes array for merchant + */ +struct ScopePermissionMap scope_permissions[] = { + /* Deprecated since v19 */ { - *as = TMH_AS_NONE; - return TALER_EC_NONE; + .as = TMH_AS_ADMIN, + .name = "write", + .permissions = "*" + }, + /* Full access */ + { + .as = TMH_AS_ADMIN, + .name = "admin", + .permissions = "*" + }, + /* Read-only access */ + { + .as = TMH_AS_READ_ONLY, + .name = "readonly", + .permissions = "*-read" + }, + /* Simple order management */ + { + .as = TMH_AS_ORDER_SIMPLE, + .name = "order-simple", + .permissions = "orders-read,orders-write" + }, + /* Simple order management for PoS, also allows inventory locking */ + { + .as = TMH_AS_ORDER_POS, + .name = "order-pos", + .permissions = "orders-read,orders-write,inventory-lock" + }, + /* Simple order management, also allows refunding */ + { + .as = TMH_AS_ORDER_MGMT, + .name = "order-mgmt", + .permissions = "orders-read,orders-write,orders-refund" + }, + /* Full order management, allows inventory locking and refunds */ + { + .as = TMH_AS_ORDER_FULL, + .name = "order-full", + .permissions = "orders-read,orders-write,inventory-lock,orders-refund" + }, + /* No permissions, dummy scope */ + { + .as = TMH_AS_NONE, } - /* This was presumably checked before... */ - GNUNET_assert (0 == strncasecmp (token, - RFC_8959_PREFIX, - strlen (RFC_8959_PREFIX))); - token += strlen (RFC_8959_PREFIX); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (token, - strlen (token), - &btoken, - sizeof (btoken))) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Given authorization token `%s' is malformed\n", - token); +}; + + +/** + * Get permissions string for scope. + * Also extracts the leftmost bit into the @a refreshable + * output parameter. + * + * @param as the scope to get the permissions string from + * @param[out] refreshable true if the token associated with this scope is refreshable. + * @return the permissions string, or NULL if no such scope found + */ +static const char* +get_scope_permissions (enum TMH_AuthScope as, + bool *refreshable) +{ + *refreshable = as & TMH_AS_REFRESHABLE; + for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++) + { + /* We ignore the TMH_AS_REFRESHABLE bit */ + if ( (as & ~TMH_AS_REFRESHABLE) == + (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) ) + return scope_permissions[i].permissions; + } + return NULL; +} + + +/** + * Checks if @a permission_required is in permissions of + * @a scope. + * + * @param permission_required the permission to check. + * @param scope the scope to check. + * @return true if @a permission_required is in the permissions set of @a scope. + */ +static bool +permission_in_scope (const char *permission_required, + enum TMH_AuthScope scope) +{ + char *permissions; + const char *perms_tmp; + bool is_read_perm; + bool is_write_perm; + bool refreshable; + const char *last_dash; + + perms_tmp = get_scope_permissions (scope, + &refreshable); + if (NULL == perms_tmp) + { GNUNET_break_op (0); - return TALER_EC_GENERIC_TOKEN_MALFORMED; + return false; } - qs = TMH_db->select_login_token (TMH_db->cls, - instance_id, - &btoken, - &expiration, - &scope); - if (qs < 0) + last_dash = strrchr (permission_required, + '-'); + if (NULL != last_dash) { - GNUNET_break (0); - return TALER_EC_GENERIC_DB_FETCH_FAILED; + is_write_perm = (0 == strcmp (last_dash, + "-write")); + is_read_perm = (0 == strcmp (last_dash, + "-read")); } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + + if (refreshable && + (0 == strcmp ("token-refresh", + permission_required)) ) + return true; + permissions = GNUNET_strdup (perms_tmp); { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Authorization token `%s' unknown\n", - token); - return TALER_EC_GENERIC_TOKEN_UNKNOWN; + const char *perm = strtok (permissions, + ","); + + if (NULL == perm) + { + GNUNET_free (permissions); + return false; + } + while (NULL != perm) + { + if (0 == strcmp ("*", + perm)) + { + GNUNET_free (permissions); + return true; + } + if ( (0 == strcmp ("*-write", + perm)) && + (is_write_perm) ) + { + GNUNET_free (permissions); + return true; + } + if ( (0 == strcmp ("*-read", + perm)) && + (is_read_perm) ) + { + GNUNET_free (permissions); + return true; + } + if (0 == strcmp (permission_required, + perm)) + { + GNUNET_free (permissions); + return true; + } + perm = strtok (NULL, + ","); + } } - if (GNUNET_TIME_absolute_is_past (expiration.abs_time)) + GNUNET_free (permissions); + return false; +} + + +bool +TMH_scope_is_subset (enum TMH_AuthScope as, + enum TMH_AuthScope candidate) +{ + const char *as_perms; + const char *candidate_perms; + char *permissions; + bool as_refreshable; + bool cand_refreshable; + + as_perms = get_scope_permissions (as, + &as_refreshable); + candidate_perms = get_scope_permissions (candidate, + &cand_refreshable); + if (! as_refreshable && cand_refreshable) + return false; + if ( (NULL == as_perms) && + (NULL != candidate_perms) ) + return false; + if ( (NULL == candidate_perms) || + (0 == strcmp ("*", + as_perms))) + return true; + permissions = GNUNET_strdup (candidate_perms); { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Authorization token `%s' expired\n", - token); - return TALER_EC_GENERIC_TOKEN_EXPIRED; + const char *perm; + + perm = strtok (permissions, + ","); + if (NULL == perm) + { + GNUNET_free (permissions); + return true; + } + while (NULL != perm) + { + if (! permission_in_scope (perm, + as)) + { + GNUNET_free (permissions); + return false; + } + perm = strtok (NULL, + ","); + } } - *as = scope; - return TALER_EC_NONE; + GNUNET_free (permissions); + return true; +} + + +enum TMH_AuthScope +TMH_get_scope_by_name (const char *name) +{ + for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++) + { + if (0 == strcasecmp (scope_permissions[i].name, + name)) + return scope_permissions[i].as; + } + return TMH_AS_NONE; } enum GNUNET_GenericReturnValue -TMH_check_auth (const char *token, +TMH_check_auth (const char *password, struct TALER_MerchantAuthenticationSaltP *salt, struct TALER_MerchantAuthenticationHashP *hash) { - struct GNUNET_HashCode val; - char *dec; - size_t dec_len; + struct TALER_MerchantAuthenticationHashP val; if (GNUNET_is_zero (hash)) return GNUNET_OK; - if (NULL == token) + if (NULL == password) return GNUNET_SYSERR; - dec_len = GNUNET_STRINGS_urldecode (token, - strlen (token), - &dec); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Checking against token with salt %s\n", TALER_B2S (salt)); - GNUNET_assert (GNUNET_YES == - GNUNET_CRYPTO_kdf (&val, - sizeof (val), - salt, - sizeof (*salt), - dec, - dec_len, - "merchant-instance-auth", - strlen ("merchant-instance-auth"), - NULL, - 0)); - GNUNET_free (dec); - return (0 == GNUNET_memcmp (&val, - &hash->hash)) + TALER_merchant_instance_auth_hash_with_salt (&val, + salt, + password); + return (0 == + GNUNET_memcmp (&val, + hash)) ? GNUNET_OK : GNUNET_SYSERR; } +/** + * Check if @a userpass grants access to @a instance. + * + * @param userpass base64 encoded "$USERNAME:$PASSWORD" value + * from HTTP Basic "Authentication" header + * @param instances the access controlled instance + */ +static enum GNUNET_GenericReturnValue +check_auth_instance (const char *userpass, + struct TMH_MerchantInstance *instance) +{ + char *tmp; + char *colon; + const char *instance_name; + const char *password; + const char *target_instance = "admin"; + enum GNUNET_GenericReturnValue ret; + + /* implicitly a zeroed out hash means no authentication */ + if (GNUNET_is_zero (&instance->auth.auth_hash)) + return GNUNET_OK; + if (NULL == userpass) + return GNUNET_SYSERR; + if (0 == + GNUNET_STRINGS_base64_decode (userpass, + strlen (userpass), + (void**) &tmp)) + { + return GNUNET_SYSERR; + } + colon = strchr (tmp, + ':'); + if (NULL == colon) + { + GNUNET_free (tmp); + return GNUNET_SYSERR; + } + *colon = '\0'; + instance_name = tmp; + password = colon + 1; + /* instance->settings.id can be NULL if there is no instance yet */ + if (NULL != instance->settings.id) + target_instance = instance->settings.id; + if (0 != strcmp (instance_name, + target_instance)) + { + GNUNET_free (tmp); + return GNUNET_SYSERR; + } + ret = TMH_check_auth (password, + &instance->auth.auth_salt, + &instance->auth.auth_hash); + GNUNET_free (tmp); + return ret; +} + + void TMH_compute_auth (const char *token, struct TALER_MerchantAuthenticationSaltP *salt, @@ -328,17 +566,9 @@ TMH_compute_auth (const char *token, GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Computing initial auth using token with salt %s\n", TALER_B2S (salt)); - GNUNET_assert (GNUNET_YES == - GNUNET_CRYPTO_kdf (hash, - sizeof (*hash), - salt, - sizeof (*salt), - token, - strlen (token), - "merchant-instance-auth", - strlen ("merchant-instance-auth"), - NULL, - 0)); + TALER_merchant_instance_auth_hash_with_salt (hash, + salt, + token); } @@ -615,7 +845,8 @@ spa_redirect (const struct TMH_RequestHandler *rh, GNUNET_break (0); return MHD_NO; } - TALER_MHD_add_global_headers (response); + TALER_MHD_add_global_headers (response, + true); GNUNET_break (MHD_YES == MHD_add_response_header (response, MHD_HTTP_HEADER_CONTENT_TYPE, @@ -654,30 +885,58 @@ spa_redirect (const struct TMH_RequestHandler *rh, /** * Extract the token from authorization header value @a auth. + * The @a auth value can be a bearer token or a Basic + * authentication header. In both cases, this function + * updates @a auth to point to the actual credential, + * skipping spaces. * - * @param auth pointer to authorization header value, + * NOTE: We probably want to replace this function with MHD2 + * API calls in the future that are more robust. + * + * @param[in,out] auth pointer to authorization header value, * will be updated to point to the start of the token * or set to NULL if header value is invalid + * @param[out] is_basic_auth will be set to true if the + * authorization header uses basic authentication, + * otherwise to false */ static void -extract_token (const char **auth) +extract_auth (const char **auth, + bool *is_basic_auth) { const char *bearer = "Bearer "; + const char *basic = "Basic "; const char *tok = *auth; + size_t offset = 0; + bool is_bearer = false; - if (0 != strncmp (tok, + *is_basic_auth = false; + if (0 == strncmp (tok, bearer, strlen (bearer))) { + offset = strlen (bearer); + is_bearer = true; + } + else if (0 == strncmp (tok, + basic, + strlen (basic))) + { + offset = strlen (basic); + *is_basic_auth = true; + } + else + { *auth = NULL; return; } - tok += strlen (bearer); + tok += offset; while (' ' == *tok) tok++; - if (0 != strncasecmp (tok, - RFC_8959_PREFIX, - strlen (RFC_8959_PREFIX))) + if ( (is_bearer) && + (0 != strncasecmp (tok, + RFC_8959_PREFIX, + strlen (RFC_8959_PREFIX))) ) { *auth = NULL; return; @@ -826,6 +1085,7 @@ url_handler (void *cls, { .url_prefix = "/instances", .method = MHD_HTTP_METHOD_GET, + .permission = "instances-write", .skip_instance = true, .default_only = true, .handler = &TMH_private_get_instances @@ -834,6 +1094,7 @@ url_handler (void *cls, { .url_prefix = "/instances", .method = MHD_HTTP_METHOD_POST, + .permission = "instances-write", .skip_instance = true, .default_only = true, .handler = &TMH_private_post_instances, @@ -847,6 +1108,7 @@ url_handler (void *cls, { .url_prefix = "/instances/", .method = MHD_HTTP_METHOD_GET, + .permission = "instances-write", .skip_instance = true, .default_only = true, .have_id_segment = true, @@ -856,6 +1118,7 @@ url_handler (void *cls, { .url_prefix = "/instances/", .method = MHD_HTTP_METHOD_DELETE, + .permission = "instances-write", .skip_instance = true, .default_only = true, .have_id_segment = true, @@ -865,6 +1128,7 @@ url_handler (void *cls, { .url_prefix = "/instances/", .method = MHD_HTTP_METHOD_PATCH, + .permission = "instances-write", .skip_instance = true, .default_only = true, .have_id_segment = true, @@ -880,6 +1144,7 @@ url_handler (void *cls, .url_prefix = "/instances/", .url_suffix = "auth", .method = MHD_HTTP_METHOD_POST, + .permission = "instances-auth-write", .skip_instance = true, .default_only = true, .have_id_segment = true, @@ -892,6 +1157,7 @@ url_handler (void *cls, .url_prefix = "/instances/", .url_suffix = "kyc", .method = MHD_HTTP_METHOD_GET, + .permission = "instances-kyc-read", .skip_instance = true, .default_only = true, .have_id_segment = true, @@ -907,12 +1173,14 @@ url_handler (void *cls, { .url_prefix = "/", .method = MHD_HTTP_METHOD_GET, + .permission = "instances-read", .handler = &TMH_private_get_instances_ID }, /* DELETE /instances/$ID/: */ { .url_prefix = "/", .method = MHD_HTTP_METHOD_DELETE, + .permission = "instances-write", .allow_deleted_instance = true, .handler = &TMH_private_delete_instances_ID }, @@ -921,6 +1189,7 @@ url_handler (void *cls, .url_prefix = "/", .method = MHD_HTTP_METHOD_PATCH, .handler = &TMH_private_patch_instances_ID, + .permission = "instances-write", .allow_deleted_instance = true, /* allow instance data of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) @@ -933,6 +1202,7 @@ url_handler (void *cls, .url_prefix = "/auth", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_instances_ID_auth, + .permission = "auth-write", /* Body should be pretty small. */ .max_upload = 1024 * 1024, }, @@ -940,24 +1210,28 @@ url_handler (void *cls, { .url_prefix = "/kyc", .method = MHD_HTTP_METHOD_GET, + .permission = "kyc-read", .handler = &TMH_private_get_instances_ID_kyc, }, /* GET /pos: */ { .url_prefix = "/pos", .method = MHD_HTTP_METHOD_GET, + .permission = "pos-read", .handler = &TMH_private_get_pos }, /* GET /categories: */ { .url_prefix = "/categories", .method = MHD_HTTP_METHOD_GET, + .permission = "categories-read", .handler = &TMH_private_get_categories }, /* POST /categories: */ { .url_prefix = "/categories", .method = MHD_HTTP_METHOD_POST, + .permission = "categories-write", .handler = &TMH_private_post_categories, /* allow category data of up to 8 kb, that should be plenty */ .max_upload = 1024 * 8 @@ -966,6 +1240,7 @@ url_handler (void *cls, { .url_prefix = "/categories/", .method = MHD_HTTP_METHOD_GET, + .permission = "categories-read", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_get_categories_ID @@ -974,6 +1249,7 @@ url_handler (void *cls, { .url_prefix = "/categories/", .method = MHD_HTTP_METHOD_DELETE, + .permission = "categories-write", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_delete_categories_ID @@ -982,6 +1258,7 @@ url_handler (void *cls, { .url_prefix = "/categories/", .method = MHD_HTTP_METHOD_PATCH, + .permission = "categories-write", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_patch_categories_ID, @@ -991,6 +1268,7 @@ url_handler (void *cls, /* GET /products: */ { .url_prefix = "/products", + .permission = "products-read", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_products }, @@ -998,6 +1276,7 @@ url_handler (void *cls, { .url_prefix = "/products", .method = MHD_HTTP_METHOD_POST, + .permission = "products-write", .handler = &TMH_private_post_products, /* allow product data of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) @@ -1010,6 +1289,7 @@ url_handler (void *cls, .url_prefix = "/products/", .method = MHD_HTTP_METHOD_GET, .have_id_segment = true, + .permission = "products-read", .allow_deleted_instance = true, .handler = &TMH_private_get_products_ID }, @@ -1018,6 +1298,7 @@ url_handler (void *cls, .url_prefix = "/products/", .method = MHD_HTTP_METHOD_DELETE, .have_id_segment = true, + .permission = "products-write", .allow_deleted_instance = true, .handler = &TMH_private_delete_products_ID }, @@ -1027,6 +1308,7 @@ url_handler (void *cls, .method = MHD_HTTP_METHOD_PATCH, .have_id_segment = true, .allow_deleted_instance = true, + .permission = "products-write", .handler = &TMH_private_patch_products_ID, /* allow product data of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) @@ -1040,6 +1322,7 @@ url_handler (void *cls, .url_suffix = "lock", .method = MHD_HTTP_METHOD_POST, .have_id_segment = true, + .permission = "products-lock", .handler = &TMH_private_post_products_ID_lock, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ @@ -1049,6 +1332,7 @@ url_handler (void *cls, { .url_prefix = "/orders", .method = MHD_HTTP_METHOD_POST, + .permission = "orders-write", .handler = &TMH_private_post_orders, /* allow contracts of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) @@ -1060,6 +1344,7 @@ url_handler (void *cls, { .url_prefix = "/orders/", .method = MHD_HTTP_METHOD_GET, + .permission = "orders-read", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_get_orders_ID @@ -1068,6 +1353,7 @@ url_handler (void *cls, { .url_prefix = "/orders", .method = MHD_HTTP_METHOD_GET, + .permission = "orders-read", .allow_deleted_instance = true, .handler = &TMH_private_get_orders }, @@ -1077,6 +1363,7 @@ url_handler (void *cls, .url_suffix = "refund", .method = MHD_HTTP_METHOD_POST, .have_id_segment = true, + .permission = "orders-refund", .handler = &TMH_private_post_orders_ID_refund, /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ @@ -1087,6 +1374,7 @@ url_handler (void *cls, .url_prefix = "/orders/", .url_suffix = "forget", .method = MHD_HTTP_METHOD_PATCH, + .permission = "orders-write", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_patch_orders_ID_forget, @@ -1098,6 +1386,7 @@ url_handler (void *cls, { .url_prefix = "/orders/", .method = MHD_HTTP_METHOD_DELETE, + .permission = "orders-write", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_delete_orders_ID @@ -1108,6 +1397,7 @@ url_handler (void *cls, .method = MHD_HTTP_METHOD_POST, .allow_deleted_instance = true, .handler = &TMH_private_post_transfers, + .permission = "transfers-write", /* the body should be pretty small, allow 1 MB of upload to set a conservative bound for sane wallets */ .max_upload = 1024 * 1024 @@ -1116,6 +1406,7 @@ url_handler (void *cls, { .url_prefix = "/transfers/", .method = MHD_HTTP_METHOD_DELETE, + .permission = "transfers-write", .allow_deleted_instance = true, .handler = &TMH_private_delete_transfers_ID, .have_id_segment = true, @@ -1126,6 +1417,7 @@ url_handler (void *cls, /* GET /transfers: */ { .url_prefix = "/transfers", + .permission = "transfers-read", .method = MHD_HTTP_METHOD_GET, .allow_deleted_instance = true, .handler = &TMH_private_get_transfers @@ -1133,12 +1425,14 @@ url_handler (void *cls, /* POST /otp-devices: */ { .url_prefix = "/otp-devices", + .permission = "otp-devices-write", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_otp_devices }, /* GET /otp-devices: */ { .url_prefix = "/otp-devices", + .permission = "opt-devices-read", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_otp_devices }, @@ -1146,6 +1440,7 @@ url_handler (void *cls, { .url_prefix = "/otp-devices/", .method = MHD_HTTP_METHOD_GET, + .permission = "otp-devices-read", .have_id_segment = true, .handler = &TMH_private_get_otp_devices_ID }, @@ -1153,6 +1448,7 @@ url_handler (void *cls, { .url_prefix = "/otp-devices/", .method = MHD_HTTP_METHOD_DELETE, + .permission = "otp-devices-write", .have_id_segment = true, .handler = &TMH_private_delete_otp_devices_ID }, @@ -1160,6 +1456,7 @@ url_handler (void *cls, { .url_prefix = "/otp-devices/", .method = MHD_HTTP_METHOD_PATCH, + .permission = "otp-devices-write", .have_id_segment = true, .handler = &TMH_private_patch_otp_devices_ID }, @@ -1167,6 +1464,7 @@ url_handler (void *cls, { .url_prefix = "/templates", .method = MHD_HTTP_METHOD_POST, + .permission = "templates-write", .handler = &TMH_private_post_templates, /* allow template data of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) @@ -1177,6 +1475,7 @@ url_handler (void *cls, /* GET /templates: */ { .url_prefix = "/templates", + .permission = "templates-read", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_templates }, @@ -1184,6 +1483,7 @@ url_handler (void *cls, { .url_prefix = "/templates/", .method = MHD_HTTP_METHOD_GET, + .permission = "templates-read", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_get_templates_ID @@ -1192,6 +1492,7 @@ url_handler (void *cls, { .url_prefix = "/templates/", .method = MHD_HTTP_METHOD_DELETE, + .permission = "templates-write", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_delete_templates_ID @@ -1200,6 +1501,7 @@ url_handler (void *cls, { .url_prefix = "/templates/", .method = MHD_HTTP_METHOD_PATCH, + .permission = "templates-write", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_patch_templates_ID, @@ -1212,6 +1514,7 @@ url_handler (void *cls, /* GET /webhooks: */ { .url_prefix = "/webhooks", + .permission = "webhooks-read", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_webhooks }, @@ -1219,6 +1522,7 @@ url_handler (void *cls, { .url_prefix = "/webhooks", .method = MHD_HTTP_METHOD_POST, + .permission = "webhooks-write", .handler = &TMH_private_post_webhooks, /* allow webhook data of up to 8 MB, that should be plenty; note that exceeding #GNUNET_MAX_MALLOC_CHECKED (40 MB) @@ -1230,6 +1534,7 @@ url_handler (void *cls, { .url_prefix = "/webhooks/", .method = MHD_HTTP_METHOD_GET, + .permission = "webhooks-read", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_get_webhooks_ID @@ -1237,6 +1542,7 @@ url_handler (void *cls, /* DELETE /webhooks/$ID/: */ { .url_prefix = "/webhooks/", + .permission = "webhooks-write", .method = MHD_HTTP_METHOD_DELETE, .have_id_segment = true, .allow_deleted_instance = true, @@ -1246,6 +1552,7 @@ url_handler (void *cls, { .url_prefix = "/webhooks/", .method = MHD_HTTP_METHOD_PATCH, + .permission = "webhooks-write", .have_id_segment = true, .allow_deleted_instance = true, .handler = &TMH_private_patch_webhooks_ID, @@ -1259,6 +1566,7 @@ url_handler (void *cls, { .url_prefix = "/accounts", .method = MHD_HTTP_METHOD_POST, + .permission = "accounts-write", .handler = &TMH_private_post_account, /* allow account details of up to 8 kb, that should be plenty */ .max_upload = 1024 * 8 @@ -1267,6 +1575,7 @@ url_handler (void *cls, { .url_prefix = "/accounts/", .method = MHD_HTTP_METHOD_PATCH, + .permission = "accounts-write", .handler = &TMH_private_patch_accounts_ID, .have_id_segment = true, /* allow account details of up to 8 kb, that should be plenty */ @@ -1275,12 +1584,14 @@ url_handler (void *cls, /* GET /accounts: */ { .url_prefix = "/accounts", + .permission = "accounts-read", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_accounts }, /* GET /accounts/$H_WIRE: */ { .url_prefix = "/accounts/", + .permission = "accounts-read", .method = MHD_HTTP_METHOD_GET, .have_id_segment = true, .handler = &TMH_private_get_accounts_ID @@ -1288,6 +1599,7 @@ url_handler (void *cls, /* DELETE /accounts/$H_WIRE: */ { .url_prefix = "/accounts/", + .permission = "accounts-write", .method = MHD_HTTP_METHOD_DELETE, .handler = &TMH_private_delete_account_ID, .have_id_segment = true @@ -1295,7 +1607,7 @@ url_handler (void *cls, /* POST /token: */ { .url_prefix = "/token", - .auth_scope = TMH_AS_REFRESHABLE, + .permission = "token-refresh", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_instances_ID_token, /* Body should be tiny. */ @@ -1304,19 +1616,20 @@ url_handler (void *cls, /* DELETE /token: */ { .url_prefix = "/token", - .auth_scope = TMH_AS_READ_ONLY, .method = MHD_HTTP_METHOD_DELETE, .handler = &TMH_private_delete_instances_ID_token, }, /* GET /tokenfamilies: */ { .url_prefix = "/tokenfamilies", + .permission = "tokenfamilies-read", .method = MHD_HTTP_METHOD_GET, .handler = &TMH_private_get_tokenfamilies }, /* POST /tokenfamilies: */ { .url_prefix = "/tokenfamilies", + .permission = "tokenfamilies-write", .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_token_families }, @@ -1324,6 +1637,7 @@ url_handler (void *cls, { .url_prefix = "/tokenfamilies/", .method = MHD_HTTP_METHOD_GET, + .permission = "tokenfamilies-read", .have_id_segment = true, .handler = &TMH_private_get_tokenfamilies_SLUG }, @@ -1331,6 +1645,7 @@ url_handler (void *cls, { .url_prefix = "/tokenfamilies/", .method = MHD_HTTP_METHOD_DELETE, + .permission = "tokenfamilies-write", .have_id_segment = true, .handler = &TMH_private_delete_token_families_SLUG }, @@ -1338,6 +1653,7 @@ url_handler (void *cls, { .url_prefix = "/tokenfamilies/", .method = MHD_HTTP_METHOD_PATCH, + .permission = "tokenfamilies-write", .have_id_segment = true, .handler = &TMH_private_patch_token_family_SLUG, }, @@ -1366,6 +1682,7 @@ url_handler (void *cls, { .url_prefix = "/statistics-counter/", .method = MHD_HTTP_METHOD_GET, + .permission = "statistics-read", .have_id_segment = true, .handler = &TMH_private_get_statistics_counter_SLUG, }, @@ -1373,6 +1690,7 @@ url_handler (void *cls, { .url_prefix = "/statistics-amount/", .method = MHD_HTTP_METHOD_GET, + .permission = "statistics-read", .have_id_segment = true, .handler = &TMH_private_get_statistics_amount_SLUG, }, @@ -1649,7 +1967,8 @@ url_handler (void *cls, = MHD_create_response_from_buffer (0, NULL, MHD_RESPMEM_PERSISTENT); - TALER_MHD_add_global_headers (response); + TALER_MHD_add_global_headers (response, + true); if (MHD_NO == MHD_add_response_header (response, MHD_HTTP_HEADER_LOCATION, @@ -1674,20 +1993,6 @@ url_handler (void *cls, (0 == strcmp ("admin", instance_id)) ) hc->instance = TMH_lookup_instance (NULL); - if ( (0 == strcmp ("admin", - instance_id)) && - (NULL != TMH_default_auth) && - (NULL != hc->instance) ) - { - /* Override default instance access control */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Command-line override of access control\n"); - TMH_compute_auth (TMH_default_auth, - &hc->instance->auth.auth_salt, - &hc->instance->auth.auth_hash); - hc->instance->auth_override = true; - GNUNET_free (TMH_default_auth); - } GNUNET_free (instance_id); if (NULL == slash) url = ""; @@ -1699,18 +2004,6 @@ url_handler (void *cls, /* use 'default' */ use_default = true; hc->instance = TMH_lookup_instance (NULL); - if ( (NULL != TMH_default_auth) && - (NULL != hc->instance) ) - { - /* Override default instance access control */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Command-line override of access control\n"); - TMH_compute_auth (TMH_default_auth, - &hc->instance->auth.auth_salt, - &hc->instance->auth.auth_hash); - hc->instance->auth_override = true; - GNUNET_free (TMH_default_auth); - } } if (NULL != hc->instance) { @@ -1920,14 +2213,13 @@ url_handler (void *cls, if (public_handlers != handlers) { const char *auth; - bool auth_ok; + bool is_basic_auth = false; bool auth_malformed = false; - /* PATCHing an instance can alternatively be checked against - the default instance */ auth = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_AUTHORIZATION); + if (NULL != auth) { /* We _only_ complain about malformed auth headers if @@ -1936,70 +2228,132 @@ url_handler (void *cls, because some reverse proxy is already doing it, and then that reverse proxy may forward malformed auth headers to the backend. */ - extract_token (&auth); + extract_auth (&auth, + &is_basic_auth); if (NULL == auth) auth_malformed = true; hc->auth_token = auth; } /* If we have zero configured instances (not even ones that have been - purged) AND no override credentials, THEN we accept anything (no access + purged), THEN we accept anything (no access control), as we then also have no data to protect. */ - auth_ok = ( (0 == - GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) && - (NULL == TMH_default_auth) ); - /* Check against selected instance, if we have one */ - if (NULL != hc->instance) - auth_ok |= (GNUNET_OK == - TMH_check_auth (auth, - &hc->instance->auth.auth_salt, - &hc->instance->auth.auth_hash)); - else /* Are the credentials provided OK for CLI override? */ - auth_ok |= (use_default && - (NULL != TMH_default_auth) && - (NULL != auth) && - (! auth_malformed) && - (0 == strcmp (auth, - TMH_default_auth)) ); - if (auth_ok) - { - hc->auth_scope = TMH_AS_ALL; + if (0 == GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) + { + hc->auth_scope = TMH_AS_ADMIN; } - else + else if (is_basic_auth) + { + /* Handle token endpoint slightly differently: Only allow + * instance password (Basic auth) to retrieve access token. + * We need to handle authorization with Basic auth here first + * The only time we need to handle authentication like this is + * for the token endpoint! + */ + if ( (0 != strncmp (hc->rh->url_prefix, + "/token", + strlen ("/token"))) || + (0 != strncmp (MHD_HTTP_METHOD_POST, + hc->rh->method, + strlen (MHD_HTTP_METHOD_POST))) || + (NULL == hc->instance)) + { + // FIXME this should never happen, but according to the comment below, + // We must not error out here for some reason that has to do with + // disabled authZ behind reverse proxy...? + hc->auth_scope = TMH_AS_NONE; + } + else + { + if (GNUNET_OK == + check_auth_instance (auth, + hc->instance)) + hc->auth_scope = TMH_AS_ADMIN; + else + hc->auth_scope = TMH_AS_NONE; + } + } + else /* Check bearer token */ { if (NULL != hc->instance) { - enum TALER_ErrorCode ec; - - ec = TMH_check_token (auth, - hc->instance->settings.id, - &hc->auth_scope); - if (TALER_EC_NONE != ec) - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); + if (GNUNET_is_zero (&hc->instance->auth.auth_hash)) + { + /* hash zero means no authentication for instance */ + hc->auth_scope = TMH_AS_ADMIN; + } + else + { + enum TALER_ErrorCode ec; + + ec = TMH_check_token (auth, + hc->instance->settings.id, + &hc->auth_scope); + if (TALER_EC_NONE != ec) + { + char *dec; + size_t dec_len; + const char *token; + + /* NOTE: Deprecated, remove sometime after v1.1 */ + if (0 != strncasecmp (auth, + RFC_8959_PREFIX, + strlen (RFC_8959_PREFIX))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Trying deprecated secret-token:password API authN\n"); + token = auth + strlen (RFC_8959_PREFIX); + dec_len = GNUNET_STRINGS_urldecode (token, + strlen (token), + &dec); + if ( (0 == dec_len) || + (GNUNET_OK != + TMH_check_auth (dec, + &hc->instance->auth.auth_salt, + &hc->instance->auth.auth_hash)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Login failed\n"); + hc->auth_scope = TMH_AS_NONE; + } + else + { + hc->auth_scope = TMH_AS_ADMIN; + } + GNUNET_free (dec); + } + } } else + { hc->auth_scope = TMH_AS_NONE; + } } /* We grant access if: - scope is 'all' - rh has an explicit non-NONE scope that matches - scope is 'read only' and we have a GET request */ - if (! ( (TMH_AS_ALL == hc->auth_scope) || - ( (TMH_AS_NONE != hc->rh->auth_scope) && - (hc->rh->auth_scope == (hc->rh->auth_scope & hc->auth_scope)) ) || - ( (TMH_AS_READ_ONLY == (hc->auth_scope & TMH_AS_READ_ONLY)) && - (0 == strcmp (MHD_HTTP_METHOD_GET, - method)) ) ) ) + if ( (NULL != hc->rh->permission) && + (! permission_in_scope (hc->rh->permission, + hc->auth_scope))) { if (auth_malformed && (TMH_AS_NONE == hc->auth_scope) ) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_UNAUTHORIZED, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "'" RFC_8959_PREFIX - "' prefix or 'Bearer' missing in 'Authorization' header"); + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_UNAUTHORIZED, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'" RFC_8959_PREFIX + "' prefix or 'Bearer' missing in 'Authorization' header"); + } + GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_UNAUTHORIZED, TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, @@ -2109,7 +2463,6 @@ add_instance_cb (void *cls, &mi->h_instance, mi); } - mi = GNUNET_new (struct TMH_MerchantInstance); mi->settings = *is; mi->auth = *ias; @@ -2291,27 +2644,10 @@ run (void *cls, { enum TALER_MHD_GlobalOptions go; int elen; - const char *tok; (void) cls; (void) args; (void) cfgfile; - tok = getenv ("TALER_MERCHANT_TOKEN"); - if ( (NULL != tok) && - (NULL == TMH_default_auth) ) - TMH_default_auth = GNUNET_strdup (tok); - if ( (NULL != TMH_default_auth) && - (0 != strncmp (TMH_default_auth, - RFC_8959_PREFIX, - strlen (RFC_8959_PREFIX))) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Authentication token does not start with `%s' prefix\n", - RFC_8959_PREFIX); - global_ret = EXIT_NOTCONFIGURED; - GNUNET_SCHEDULER_shutdown (); - return; - } cfg = config; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Starting taler-merchant-httpd\n"); @@ -2361,6 +2697,16 @@ run (void *cls, return; } + if (GNUNET_SYSERR != + (TMH_strict_v19 = GNUNET_CONFIGURATION_get_value_yesno (cfg, + "merchant", + "STRICT_PROTOCOL_V19"))) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "merchant", + "STRICT_PROTOCOL_V19"); + TMH_strict_v19 = GNUNET_NO; + } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_time (cfg, "merchant", @@ -2512,11 +2858,6 @@ main (int argc, &merchant_connection_close), GNUNET_GETOPT_option_timetravel ('T', "timetravel"), - GNUNET_GETOPT_option_string ('a', - "auth", - "TOKEN", - "use TOKEN to initially authenticate access to the default instance (you can also set the TALER_MERCHANT_TOKEN environment variable instead)", - &TMH_default_auth), GNUNET_GETOPT_option_version (PACKAGE_VERSION "-" VCS_VERSION), GNUNET_GETOPT_OPTION_END }; diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -178,7 +178,7 @@ struct TMH_MerchantInstance /** * The authentication settings for this instance - * were changed via the command-line. Do not check + * do not apply due to administrative action. Do not check * against the DB value when updating the auth token. */ bool auth_override; @@ -424,14 +424,43 @@ enum TMH_AuthScope TMH_AS_READ_ONLY = 1, /** - * /login access to renew the token is OK. + * 2 is Reserved. Was refreshable pre v42 + */ + + /** + * Order creation and payment status check only + */ + TMH_AS_ORDER_SIMPLE = 3, + + /** + * Order creation and inventory locking, + * includes #TMH_AS_ORDER_SIMPLE + */ + TMH_AS_ORDER_POS = 4, + + /** + * Order creation and refund + */ + TMH_AS_ORDER_MGMT = 5, + + /** + * Order full + * Includes #TMH_ORDER_POS and #TMH_ORDER_MGMT */ - TMH_AS_REFRESHABLE = 2, + TMH_AS_ORDER_FULL = 6, /** * Full access is granted to everything. */ - TMH_AS_ALL = 7 + TMH_AS_ADMIN = 7 | 1 << 30, + + /** + * /login access to renew the token is OK. + * This is actually combined with other scopes + * and not (usually) used as a scope itself. + */ + TMH_AS_REFRESHABLE = 1 << 30, + }; @@ -456,11 +485,9 @@ struct TMH_RequestHandler const char *url_prefix; /** - * Required authentication scope for this request. NONE implies that - * #TMH_AS_ALL is required unless this is a #MHD_HTTP_METHOD_GET method, in which - * case #TMH_AS_READ_ONLY is sufficient. + * Required access permission for this request. */ - enum TMH_AuthScope auth_scope; + const char *permission; /** * Does this request include an identifier segment @@ -723,11 +750,6 @@ extern struct GNUNET_CONTAINER_MultiHashMap *TMH_by_id_map; */ extern struct GNUNET_TIME_Relative TMH_legal_expiration; -/** - * Initial authorization token. - */ -extern char *TMH_default_auth; - /** * Callback that frees an instances removing @@ -812,14 +834,33 @@ TMH_check_auth (const char *token, * Compute a @a hash from @a token hashes for * merchant instance authentication. * - * @param token the token to check + * @param password the password to check * @param[out] salt set to a fresh random salt * @param[out] hash set to the hash of @a token under @a salt */ void -TMH_compute_auth (const char *token, +TMH_compute_auth (const char *password, struct TALER_MerchantAuthenticationSaltP *salt, struct TALER_MerchantAuthenticationHashP *hash); +/** + * Check if @a candidate permissions are a subset of @a as permissions + * + * @param as scope to check against + * @param candidate scope to check if its permissions are a subset of @a as permissions. + * @return true if it was a subset, false otherwise. + */ +bool +TMH_scope_is_subset (enum TMH_AuthScope as, enum TMH_AuthScope candidate); + +/** + * Return the TMH_AuthScope corresponding to @a name. + * + * @param name the name to look for + * @return the scope corresponding to the name, or TMH_AS_NONE. + */ +enum TMH_AuthScope +TMH_get_scope_by_name (const char *name); + #endif diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c @@ -43,7 +43,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "18:1:15" +#define MERCHANT_PROTOCOL_VERSION "19:0:16" /** diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c @@ -529,16 +529,80 @@ TMH_setup_wire_account ( } +enum TALER_ErrorCode +TMH_check_token (const char *token, + const char *instance_id, + enum TMH_AuthScope *as) +{ + enum TMH_AuthScope scope; + struct GNUNET_TIME_Timestamp expiration; + enum GNUNET_DB_QueryStatus qs; + struct TALER_MERCHANTDB_LoginTokenP btoken; + + if (NULL == token) + { + *as = TMH_AS_NONE; + return TALER_EC_NONE; + } + if (0 != strncasecmp (token, + RFC_8959_PREFIX, + strlen (RFC_8959_PREFIX))) + { + *as = TMH_AS_NONE; + return TALER_EC_NONE; + } + token += strlen (RFC_8959_PREFIX); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (token, + strlen (token), + &btoken, + sizeof (btoken))) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Given authorization token `%s' is malformed\n", + token); + GNUNET_break_op (0); + return TALER_EC_GENERIC_TOKEN_MALFORMED; + } + qs = TMH_db->select_login_token (TMH_db->cls, + instance_id, + &btoken, + &expiration, + (uint32_t*) &scope); + if (qs < 0) + { + GNUNET_break (0); + return TALER_EC_GENERIC_DB_FETCH_FAILED; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Authorization token `%s' unknown\n", + token); + return TALER_EC_GENERIC_TOKEN_UNKNOWN; + } + if (GNUNET_TIME_absolute_is_past (expiration.abs_time)) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Authorization token `%s' expired\n", + token); + return TALER_EC_GENERIC_TOKEN_EXPIRED; + } + *as = scope; + return TALER_EC_NONE; +} + + enum GNUNET_GenericReturnValue TMH_check_auth_config (struct MHD_Connection *connection, const json_t *jauth, - const char **auth_token) + const char **auth_password) { bool auth_wellformed = false; const char *auth_method = json_string_value (json_object_get (jauth, "method")); - *auth_token = NULL; + *auth_password = NULL; if (NULL == auth_method) { GNUNET_break_op (0); @@ -551,20 +615,33 @@ TMH_check_auth_config (struct MHD_Connection *connection, else if (0 == strcmp (auth_method, "token")) { - *auth_token = json_string_value (json_object_get (jauth, - "token")); - if (NULL == *auth_token) + json_t *pw_value; + + pw_value = json_object_get (jauth, + "password"); + if (NULL == pw_value) + { + pw_value = json_object_get (jauth, + "token"); + } + if (NULL == pw_value) { + auth_wellformed = false; GNUNET_break_op (0); } else { - if (0 != strncasecmp (RFC_8959_PREFIX, - *auth_token, - strlen (RFC_8959_PREFIX))) - GNUNET_break_op (0); - else + *auth_password = json_string_value (pw_value); + if (NULL != auth_password) + { + if (0 == strncasecmp (RFC_8959_PREFIX, + *auth_password, + strlen (RFC_8959_PREFIX))) + { + *auth_password = *auth_password + strlen (RFC_8959_PREFIX); + } auth_wellformed = true; + } } } diff --git a/src/backend/taler-merchant-httpd_helper.h b/src/backend/taler-merchant-httpd_helper.h @@ -274,5 +274,17 @@ TMH_exchange_accounts_by_method ( const struct TALER_MasterPublicKeyP *master_pub, const char *wire_method); +/** + * Check validity of login @a token for the given @a instance_id. + * + * @param token the login token given in the request + * @param instance_id the instance the login is to be checked against + * @param[out] as set to scope of the token if it is valid + * @return TALER_EC_NONE on success + */ +enum TALER_ErrorCode +TMH_check_token (const char *token, + const char *instance_id, + enum TMH_AuthScope *as); #endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -205,7 +205,7 @@ struct DepositConfirmation * private key to the corresponding age group. Might be all zeroes for no * age attestation. */ - struct TALER_AgeAttestation minimum_age_sig; + struct TALER_AgeAttestationP minimum_age_sig; /** * If a minimum age was required (i. e. pc->minimum_age is large enough), diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c @@ -49,7 +49,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, struct TMH_HandlerContext *hc) { struct TALER_MERCHANTDB_InstanceAuthSettings ias; - const char *auth_token = NULL; + const char *auth_pw = NULL; json_t *jauth = hc->request_body; { @@ -57,12 +57,12 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, ret = TMH_check_auth_config (connection, jauth, - &auth_token); + &auth_pw); if (GNUNET_OK != ret) return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } - if (NULL == auth_token) + if (NULL == auth_pw) { memset (&ias.auth_salt, 0, @@ -73,7 +73,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, } else { - TMH_compute_auth (auth_token, + TMH_compute_auth (auth_pw, &ias.auth_salt, &ias.auth_hash); } @@ -102,6 +102,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, to the authentication. */ { struct TALER_MERCHANTDB_InstanceAuthSettings db_ias; + enum TALER_ErrorCode ec; qs = TMH_db->lookup_instance_auth (TMH_db->cls, mi->settings.id, @@ -130,20 +131,23 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, break; } - if ( (NULL == TMH_default_auth) && - (! mi->auth_override) && - (GNUNET_OK != - TMH_check_auth (hc->auth_token, - &db_ias.auth_salt, - &db_ias.auth_hash)) ) + if (! mi->auth_override) { - TMH_db->rollback (TMH_db->cls); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Refusing auth change: old token does not match\n"); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_UNAUTHORIZED, - TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, - NULL); + // FIXME are we sure what the scope here is? + ec = TMH_check_token (hc->auth_token, + mi->settings.id, + &hc->auth_scope); + if (TALER_EC_NONE != ec) + { + TMH_db->rollback (TMH_db->cls); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refusing auth change: `%s'\n", + TALER_ErrorCode_get_hint (ec)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_UNAUTHORIZED, + TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, + NULL); + } } } @@ -181,14 +185,6 @@ retry: mi->auth = ias; } mi->auth_override = false; - if (0 == strcmp (mi->settings.id, - "admin")) - { - /* The default auth string should've been - cleared with the first request - for the default instance. */ - GNUNET_assert (NULL == TMH_default_auth); - } TMH_reload_instances (mi->settings.id); return TALER_MHD_reply_static (connection, MHD_HTTP_NO_CONTENT, @@ -212,9 +208,10 @@ TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh, MHD_RESULT -TMH_private_post_instances_default_ID_auth (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +TMH_private_post_instances_default_ID_auth ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi; MHD_RESULT ret; diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c @@ -33,7 +33,6 @@ */ #define DEFAULT_DURATION GNUNET_TIME_UNIT_DAYS - MHD_RESULT TMH_private_post_instances_ID_token (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, @@ -76,22 +75,31 @@ TMH_private_post_instances_ID_token (const struct TMH_RequestHandler *rh, &btoken, sizeof (btoken)); expiration_time = GNUNET_TIME_relative_to_timestamp (duration); - if (0 == strcasecmp (scope, - "readonly")) - iscope = TMH_AS_READ_ONLY; - else if (0 == strcasecmp (scope, - "write")) - iscope = TMH_AS_ALL; - else { - GNUNET_break_op (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "scope"); + char *tmp_scope; + char *scope_prefix; + char *scope_suffix; + tmp_scope = GNUNET_strdup (scope); + scope_prefix = strtok (tmp_scope, ":"); + scope_suffix = strtok (NULL, ":"); + + /* We allow <SCOPE>:REFRESHABLE syntax */ + if ((NULL != scope_suffix) && + (0 == strcasecmp (scope_suffix, "refreshable"))) + refreshable = true; + iscope = TMH_get_scope_by_name (scope_prefix); + if (TMH_AS_NONE == iscope) + { + GNUNET_break_op (0); + GNUNET_free (tmp_scope); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "scope"); + } } if (refreshable) iscope |= TMH_AS_REFRESHABLE; - if (0 != (iscope & (~hc->auth_scope))) + if (! TMH_scope_is_subset (hc->auth_scope, iscope)) { /* more permissions requested for the new token, not allowed */ GNUNET_break_op (0); @@ -132,6 +140,8 @@ TMH_private_post_instances_ID_token (const struct TMH_RequestHandler *rh, ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, + GNUNET_JSON_pack_string ("access_token", + tok), GNUNET_JSON_pack_string ("token", tok), GNUNET_JSON_pack_string ("scope", diff --git a/src/backend/taler-merchant-httpd_private-post-instances.c b/src/backend/taler-merchant-httpd_private-post-instances.c @@ -51,7 +51,7 @@ TMH_private_post_instances (const struct TMH_RequestHandler *rh, { struct TALER_MERCHANTDB_InstanceSettings is = { 0 }; struct TALER_MERCHANTDB_InstanceAuthSettings ias; - const char *auth_token = NULL; + const char *auth_password = NULL; const char *uts = "business"; struct TMH_WireMethod *wm_head = NULL; struct TMH_WireMethod *wm_tail = NULL; @@ -108,7 +108,7 @@ TMH_private_post_instances (const struct TMH_RequestHandler *rh, ret = TMH_check_auth_config (connection, jauth, - &auth_token); + &auth_password); if (GNUNET_OK != ret) { GNUNET_JSON_parse_free (spec); @@ -208,12 +208,12 @@ TMH_private_post_instances (const struct TMH_RequestHandler *rh, (NULL != is.logo && NULL != mi->settings.logo && 0 == strcmp (mi->settings.logo, is.logo))) && - ( ( (NULL != auth_token) && + ( ( (NULL != auth_password) && (GNUNET_OK == - TMH_check_auth (auth_token, + TMH_check_auth (auth_password, &mi->auth.auth_salt, &mi->auth.auth_hash)) ) || - ( (NULL == auth_token) && + ( (NULL == auth_password) && (GNUNET_YES == GNUNET_is_zero (&mi->auth.auth_hash))) ) && (1 == json_equal (mi->settings.address, @@ -244,7 +244,7 @@ TMH_private_post_instances (const struct TMH_RequestHandler *rh, } /* handle authentication token setup */ - if (NULL == auth_token) + if (NULL == auth_password) { memset (&ias.auth_salt, 0, @@ -256,7 +256,7 @@ TMH_private_post_instances (const struct TMH_RequestHandler *rh, else { /* Sets 'auth_salt' and 'auth_hash' */ - TMH_compute_auth (auth_token, + TMH_compute_auth (auth_password, &ias.auth_salt, &ias.auth_hash); } @@ -364,13 +364,6 @@ retry: TMH_add_instance (mi)); TMH_reload_instances (mi->settings.id); } - if (0 == strcmp (is.id, - "admin")) - { - GNUNET_free (TMH_default_auth); /* clear it if the default instance was - created */ - } - GNUNET_JSON_parse_free (spec); return TALER_MHD_reply_static (connection, MHD_HTTP_NO_CONTENT, diff --git a/src/backend/taler-merchant-httpd_spa.c b/src/backend/taler-merchant-httpd_spa.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2020, 2023 Taler Systems SA + Copyright (C) 2020, 2023, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -29,47 +29,9 @@ /** - * Resource from the WebUi. + * Resources of the Merchant SPA. */ -struct WebuiFile -{ - /** - * Kept in a DLL. - */ - struct WebuiFile *next; - - /** - * Kept in a DLL. - */ - struct WebuiFile *prev; - - /** - * Path this resource matches. - */ - char *path; - - /** - * SPA resource, compressed. - */ - struct MHD_Response *zspa; - - /** - * SPA resource, vanilla. - */ - struct MHD_Response *spa; - -}; - - -/** - * Resources of the WebuUI, kept in a DLL. - */ -static struct WebuiFile *webui_head; - -/** - * Resources of the WebuUI, kept in a DLL. - */ -static struct WebuiFile *webui_tail; +static struct TALER_MHD_Spa *spa; MHD_RESULT @@ -77,262 +39,28 @@ TMH_return_spa (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { - struct WebuiFile *w = NULL; const char *infix = hc->infix; if ( (NULL == infix) || (0 == strcmp (infix, "")) ) infix = "index.html"; - for (struct WebuiFile *pos = webui_head; - NULL != pos; - pos = pos->next) - if (0 == strcmp (infix, - pos->path)) - { - w = pos; - break; - } - if (NULL == w) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - hc->url); - if ( (MHD_YES == - TALER_MHD_can_compress (connection)) && - (NULL != w->zspa) ) - return MHD_queue_response (connection, - MHD_HTTP_OK, - w->zspa); - return MHD_queue_response (connection, - MHD_HTTP_OK, - w->spa); -} - - -/** - * Function called on each file to load for the WebUI. - * - * @param cls NULL - * @param dn name of the file to load - */ -static enum GNUNET_GenericReturnValue -build_webui (void *cls, - const char *dn) -{ - static struct - { - const char *ext; - const char *mime; - } mime_map[] = { - { - .ext = "css", - .mime = "text/css" - }, - { - .ext = "html", - .mime = "text/html" - }, - { - .ext = "js", - .mime = "text/javascript" - }, - { - .ext = "jpg", - .mime = "image/jpeg" - }, - { - .ext = "jpeg", - .mime = "image/jpeg" - }, - { - .ext = "png", - .mime = "image/png" - }, - { - .ext = "svg", - .mime = "image/svg+xml" - }, - { - .ext = NULL, - .mime = NULL - }, - }; - int fd; - struct stat sb; - struct MHD_Response *zspa = NULL; - struct MHD_Response *spa; - const char *ext; - const char *mime; - - (void) cls; - /* finally open template */ - fd = open (dn, - O_RDONLY); - if (-1 == fd) - { - GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, - "open", - dn); - return GNUNET_SYSERR; - } - if (0 != - fstat (fd, - &sb)) - { - GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, - "open", - dn); - GNUNET_break (0 == close (fd)); - return GNUNET_SYSERR; - } - - mime = NULL; - ext = strrchr (dn, '.'); - if (NULL == ext) - { - GNUNET_break (0 == close (fd)); - return GNUNET_OK; - } - ext++; - for (unsigned int i = 0; NULL != mime_map[i].ext; i++) - if (0 == strcasecmp (ext, - mime_map[i].ext)) - { - mime = mime_map[i].mime; - break; - } - - { - void *in; - ssize_t r; - size_t csize; - - in = GNUNET_malloc_large (sb.st_size); - if (NULL == in) - { - GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, - "malloc"); - GNUNET_break (0 == close (fd)); - return GNUNET_SYSERR; - } - r = read (fd, - in, - sb.st_size); - if ( (-1 == r) || - (sb.st_size != (size_t) r) ) - { - GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, - "read", - dn); - GNUNET_free (in); - GNUNET_break (0 == close (fd)); - return GNUNET_SYSERR; - } - csize = (size_t) r; - if (MHD_YES == - TALER_MHD_body_compress (&in, - &csize)) - { - zspa = MHD_create_response_from_buffer (csize, - in, - MHD_RESPMEM_MUST_FREE); - if (NULL != zspa) - { - if (MHD_NO == - MHD_add_response_header (zspa, - MHD_HTTP_HEADER_CONTENT_ENCODING, - "deflate")) - { - GNUNET_break (0); - MHD_destroy_response (zspa); - zspa = NULL; - } - if (NULL != mime) - GNUNET_break (MHD_YES == - MHD_add_response_header (zspa, - MHD_HTTP_HEADER_CONTENT_TYPE, - mime)); - } - } - else - { - GNUNET_free (in); - } - } - - spa = MHD_create_response_from_fd (sb.st_size, - fd); - if (NULL == spa) - { - GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, - "open", - dn); - GNUNET_break (0 == close (fd)); - if (NULL != zspa) - { - MHD_destroy_response (zspa); - zspa = NULL; - } - return GNUNET_SYSERR; - } - if (NULL != mime) - GNUNET_break (MHD_YES == - MHD_add_response_header (spa, - MHD_HTTP_HEADER_CONTENT_TYPE, - mime)); - - { - struct WebuiFile *w; - const char *fn; - - fn = strrchr (dn, '/'); - GNUNET_assert (NULL != fn); - w = GNUNET_new (struct WebuiFile); - w->path = GNUNET_strdup (fn + 1); - w->spa = spa; - w->zspa = zspa; - GNUNET_CONTAINER_DLL_insert (webui_head, - webui_tail, - w); - } - return GNUNET_OK; + return TALER_MHD_spa_handler (spa, + connection, + infix); } enum GNUNET_GenericReturnValue TMH_spa_init () { - char *dn; - - { - char *path; - - path = GNUNET_OS_installation_get_path (TALER_MERCHANT_project_data (), - GNUNET_OS_IPK_DATADIR); - if (NULL == path) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - GNUNET_asprintf (&dn, - "%s/spa/", - path); - GNUNET_free (path); - } - - if (-1 == - GNUNET_DISK_directory_scan (dn, - &build_webui, - NULL)) + spa = TALER_MHD_spa_load (TALER_MERCHANT_project_data (), + "spa/"); + if (NULL == spa) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to load WebUI from `%s'\n", - dn); - GNUNET_free (dn); + GNUNET_break (0); return GNUNET_SYSERR; } - GNUNET_free (dn); return GNUNET_OK; } @@ -347,24 +75,9 @@ get_spa_fini (void); void __attribute__ ((destructor)) get_spa_fini () { - struct WebuiFile *w; - - while (NULL != (w = webui_head)) + if (NULL != spa) { - GNUNET_CONTAINER_DLL_remove (webui_head, - webui_tail, - w); - if (NULL != w->spa) - { - MHD_destroy_response (w->spa); - w->spa = NULL; - } - if (NULL != w->zspa) - { - MHD_destroy_response (w->zspa); - w->zspa = NULL; - } - GNUNET_free (w->path); - GNUNET_free (w); + TALER_MHD_spa_free (spa); + spa = NULL; } } diff --git a/src/backenddb/pg_lookup_instances.c b/src/backenddb/pg_lookup_instances.c @@ -46,107 +46,14 @@ struct LookupInstancesContext struct PostgresClosure *pg; /** - * Instance settings, valid only during find_instances_cb(). - */ - struct TALER_MERCHANTDB_InstanceSettings is; - - /** - * Instance authentication settings, valid only during find_instances_cb(). - */ - struct TALER_MERCHANTDB_InstanceAuthSettings ias; - - /** - * Instance serial number, valid only during find_instances_cb(). - */ - uint64_t instance_serial; - - /** - * Public key of the current instance, valid only during find_instances_cb(). - */ - struct TALER_MerchantPublicKeyP merchant_pub; - - /** * Set to the return value on errors. */ enum GNUNET_DB_QueryStatus qs; - /** - * true if we only are interested in instances for which we have the private key. - */ - bool active_only; }; /** - * Helper function to run PREPARE() macro. - * - * @param pg closure to pass - * @return status of the preparation - */ -static enum GNUNET_DB_QueryStatus -prepare (struct PostgresClosure *pg) -{ - PREPARE (pg, - "lookup_instance_private_key", - "SELECT" - " merchant_priv" - " FROM merchant_keys" - " WHERE merchant_serial=$1"); - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; -} - - -/** - * We are processing an instances lookup and have the @a accounts. - * Find the private key if possible, and invoke the callback. - * - * @param lic context we are handling - */ -static void -call_cb (struct LookupInstancesContext *lic) -{ - struct PostgresClosure *pg = lic->pg; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_uint64 (&lic->instance_serial), - GNUNET_PQ_query_param_end - }; - struct TALER_MerchantPrivateKeyP merchant_priv; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("merchant_priv", - &merchant_priv), - GNUNET_PQ_result_spec_end - }; - - qs = prepare (pg); - if (qs < 0) - { - GNUNET_break (0); - lic->qs = GNUNET_DB_STATUS_HARD_ERROR; - return; - } - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_instance_private_key", - params, - rs); - if (qs < 0) - { - GNUNET_break (0); - lic->qs = GNUNET_DB_STATUS_HARD_ERROR; - return; - } - if ( (0 == qs) && - (lic->active_only) ) - return; /* skip, not interesting */ - lic->cb (lic->cb_cls, - &lic->merchant_pub, - (0 == qs) ? NULL : &merchant_priv, - &lic->is, - &lic->ias); -} - - -/** * Function to be called with the results of a SELECT statement * that has returned @a num_results results about instances. * @@ -160,68 +67,70 @@ lookup_instances_cb (void *cls, unsigned int num_results) { struct LookupInstancesContext *lic = cls; - struct PostgresClosure *pg = lic->pg; - - lic->qs = prepare (pg); - if (lic->qs < 0) - { - GNUNET_break (0); - return; - } for (unsigned int i = 0; i < num_results; i++) { + struct TALER_MERCHANTDB_InstanceSettings is; + struct TALER_MERCHANTDB_InstanceAuthSettings ias; + uint64_t instance_serial; + struct TALER_MerchantPublicKeyP merchant_pub; + struct TALER_MerchantPrivateKeyP merchant_priv; bool no_auth; bool no_salt; + bool no_priv; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ("merchant_serial", - &lic->instance_serial), + &instance_serial), GNUNET_PQ_result_spec_auto_from_type ("merchant_pub", - &lic->merchant_pub), + &merchant_pub), GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_auto_from_type ("auth_hash", - &lic->ias.auth_hash), + &ias.auth_hash), &no_auth), GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_auto_from_type ("auth_salt", - &lic->ias.auth_salt), + &ias.auth_salt), &no_salt), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("merchant_priv", + &merchant_priv), + &no_priv), GNUNET_PQ_result_spec_string ("merchant_id", - &lic->is.id), + &is.id), GNUNET_PQ_result_spec_string ("merchant_name", - &lic->is.name), + &is.name), TALER_PQ_result_spec_json ("address", - &lic->is.address), + &is.address), TALER_PQ_result_spec_json ("jurisdiction", - &lic->is.jurisdiction), + &is.jurisdiction), GNUNET_PQ_result_spec_bool ("use_stefan", - &lic->is.use_stefan), - GNUNET_PQ_result_spec_relative_time ("default_wire_transfer_delay", - &lic->is.default_wire_transfer_delay) - , + &is.use_stefan), + GNUNET_PQ_result_spec_relative_time ( + "default_wire_transfer_delay", + &is.default_wire_transfer_delay), GNUNET_PQ_result_spec_relative_time ("default_pay_delay", - &lic->is.default_pay_delay), + &is.default_pay_delay), GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_string ("website", - &lic->is.website), + &is.website), NULL), GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_string ("email", - &lic->is.email), + &is.email), NULL), GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_string ("logo", - &lic->is.logo), + &is.logo), NULL), GNUNET_PQ_result_spec_end }; - memset (&lic->ias.auth_salt, + memset (&ias.auth_salt, 0, - sizeof (lic->ias.auth_salt)); - memset (&lic->ias.auth_hash, + sizeof (ias.auth_salt)); + memset (&ias.auth_hash, 0, - sizeof (lic->ias.auth_hash)); + sizeof (ias.auth_hash)); if (GNUNET_OK != GNUNET_PQ_extract_result (result, rs, @@ -231,10 +140,12 @@ lookup_instances_cb (void *cls, lic->qs = GNUNET_DB_STATUS_HARD_ERROR; return; } - call_cb (lic); + lic->cb (lic->cb_cls, + &merchant_pub, + (no_priv) ? NULL : &merchant_priv, + &is, + &ias); GNUNET_PQ_cleanup_result (rs); - if (0 > lic->qs) - break; } } @@ -249,7 +160,6 @@ TMH_PG_lookup_instances (void *cls, struct LookupInstancesContext lic = { .cb = cb, .cb_cls = cb_cls, - .active_only = active_only, .pg = pg }; struct GNUNET_PQ_QueryParam params[] = { @@ -261,26 +171,53 @@ TMH_PG_lookup_instances (void *cls, PREPARE (pg, "lookup_instances", "SELECT" - " merchant_serial" - ",merchant_pub" - ",auth_hash" - ",auth_salt" - ",merchant_id" - ",merchant_name" - ",address" - ",jurisdiction" - ",use_stefan" - ",default_wire_transfer_delay" - ",default_pay_delay" - ",website" - ",email" - ",logo" - " FROM merchant_instances"); - qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_instances", - params, - &lookup_instances_cb, - &lic); + " mi.merchant_serial" + ",mi.merchant_pub" + ",mi.auth_hash" + ",mi.auth_salt" + ",mi.merchant_id" + ",mi.merchant_name" + ",mi.address" + ",mi.jurisdiction" + ",mi.use_stefan" + ",mi.default_wire_transfer_delay" + ",mi.default_pay_delay" + ",mi.website" + ",mi.email" + ",mi.logo" + ",mk.merchant_priv" + " FROM merchant_instances mi" + " LEFT JOIN merchant_keys mk" + " USING (merchant_serial)"); + PREPARE (pg, + "lookup_active_instances", + "SELECT " + " mi.merchant_serial" + ",mi.merchant_pub" + ",mi.auth_hash" + ",mi.auth_salt" + ",mi.merchant_id" + ",mi.merchant_name" + ",mi.address" + ",mi.jurisdiction" + ",mi.use_stefan" + ",mi.default_wire_transfer_delay" + ",mi.default_pay_delay" + ",mi.website" + ",mi.email" + ",mi.logo" + ",mk.merchant_priv" + " FROM merchant_instances mi" + " JOIN merchant_keys mk" + " USING (merchant_serial)"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + active_only + ? "lookup_active_instances" + : "lookup_instances", + params, + &lookup_instances_cb, + &lic); if (0 > lic.qs) return lic.qs; return qs; @@ -298,7 +235,6 @@ TMH_PG_lookup_instance (void *cls, struct LookupInstancesContext lic = { .cb = cb, .cb_cls = cb_cls, - .active_only = active_only, .pg = pg }; struct GNUNET_PQ_QueryParam params[] = { @@ -311,28 +247,56 @@ TMH_PG_lookup_instance (void *cls, PREPARE (pg, "lookup_instance", "SELECT" - " merchant_serial" - ",merchant_pub" - ",auth_hash" - ",auth_salt" - ",merchant_id" - ",merchant_name" - ",user_type" - ",address" - ",jurisdiction" - ",use_stefan" - ",default_wire_transfer_delay" - ",default_pay_delay" - ",website" - ",email" - ",logo" - " FROM merchant_instances" + " mi.merchant_serial" + ",mi.merchant_pub" + ",mi.auth_hash" + ",mi.auth_salt" + ",mi.merchant_id" + ",mi.merchant_name" + ",mi.address" + ",mi.jurisdiction" + ",mi.use_stefan" + ",mi.default_wire_transfer_delay" + ",mi.default_pay_delay" + ",mi.website" + ",mi.email" + ",mi.logo" + ",mk.merchant_priv" + " FROM merchant_instances mi" + " LEFT JOIN merchant_keys mk" + " USING (merchant_serial)" + " WHERE merchant_id=$1"); + PREPARE (pg, + "lookup_active_instance", + "SELECT" + " mi.merchant_serial" + ",mi.merchant_pub" + ",mi.auth_hash" + ",mi.auth_salt" + ",mi.merchant_id" + ",mi.merchant_name" + ",mi.user_type" + ",mi.address" + ",mi.jurisdiction" + ",mi.use_stefan" + ",mi.default_wire_transfer_delay" + ",mi.default_pay_delay" + ",mi.website" + ",mi.email" + ",mi.logo" + ",mk.merchant_priv" + " FROM merchant_instances mi" + " JOIN merchant_keys mk" + " USING (merchant_serial)" " WHERE merchant_id=$1"); - qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_instance", - params, - &lookup_instances_cb, - &lic); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + active_only + ? "lookup_active_instance" + : "lookup_instance", + params, + &lookup_instances_cb, + &lic); if (0 > lic.qs) return lic.qs; return qs; diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -733,6 +733,110 @@ void TALER_MERCHANT_instance_auth_post_cancel ( struct TALER_MERCHANT_InstanceAuthPostHandle *iaph); +/** + * Handle for an operation to get access token. + */ +struct TALER_MERCHANT_InstanceTokenPostHande; + + +/** + * Function called with the result of the GET /instances/$ID/private/token + * operation. + * + * @param cls closure + * @param hr HTTP response data + */ +typedef void +(*TALER_MERCHANT_InstanceTokenPostCallback)( + void *cls, + const struct TALER_MERCHANT_HttpResponse *hr); + +/** + * Get access token for an existing instance in the backend. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend (top-level "default" instance + * or base URL of an instance if @a instance_id is NULL) + * @param instance_id identity of the instance to patch the authentication for; NULL + * if the instance is identified as part of the @a backend_url + * @param scope authorization scope for token needed to access the instance, can be NULL + * @param duration requested authorization duration + * @param refreshable requesting a refreshable token or not + * @param cb function to call with the backend's response + * @param cb_cls closure for @a config_cb + * @return the instances handle; NULL upon error + */ +struct TALER_MERCHANT_InstanceTokenPostHandle * +TALER_MERCHANT_instance_token_post ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance_id, + const char *scope, + struct GNUNET_TIME_Relative duration, + bool refreshable, + TALER_MERCHANT_InstanceTokenPostCallback cb, + void *cb_cls); + + +/** + * Cancel /private/token request. Must not be called by clients after + * the callback was invoked. Afterwards, the authentication may or + * may not have been updated. + * + * @param iaph request to cancel. + */ +void +TALER_MERCHANT_instance_token_post_cancel ( + struct TALER_MERCHANT_InstanceTokenPostHandle *itph); + +/** + * Handle for a DELETE /instances/$ID/private/token operation. + */ +struct TALER_MERCHANT_InstanceTokenDeleteHandle; + + +/** + * Function called with the result of the DELETE /instances/$ID/private/token operation. + * + * @param cls closure + * @param par response data + */ +typedef void +(*TALER_MERCHANT_InstanceTokenDeleteCallback)( + void *cls, + const struct TALER_MERCHANT_HttpResponse *hr); + + +/** + * Remove token instance in the backend. + * + * @param ctx the context + * @param backend_url HTTP base URL for the backend + * @param instance_id identity of the instance to patch the authentication for; NULL + * if the instance is identified as part of the @a backend_url + * @param cb function to call with the response + * @param cb_cls closure for @a config_cb + * @return the instances handle; NULL upon error + */ +struct TALER_MERCHANT_InstanceTokenDeleteHandle * +TALER_MERCHANT_instance_token_delete ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance_id, + TALER_MERCHANT_InstanceTokenDeleteCallback cb, + void *cb_cls); + + +/** + * Cancel DELETE token request. Must not be called by clients after + * the callback was invoked. + * + * @param pah request to cancel. + */ +void +TALER_MERCHANT_instance_token_delete_cancel ( + struct TALER_MERCHANT_InstanceTokenDeleteHandle *tdh); + /** * Handle for a GET /instances/$ID operation. @@ -3467,7 +3571,7 @@ struct TALER_MERCHANT_PayCoin /** * Coin's age commitment. Might be NULL, if not applicable. */ - const struct TALER_AgeCommitmentHash *h_age_commitment; + const struct TALER_AgeCommitmentHashP *h_age_commitment; /** * Amount this coin contributes to (including fee). diff --git a/src/include/taler_merchant_testing_lib.h b/src/include/taler_merchant_testing_lib.h @@ -122,6 +122,56 @@ TALER_TESTING_cmd_merchant_post_instance_auth (const char *label, /** + * Define a "POST /private/token" CMD. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the + * POST /instances request. + * @param instance_id the ID of the instance, or NULL + * @param scope scope to request, can be NULL + * @param duration requested token validity duration + * @param refreshable should token be refreshable + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_instance_token (const char *label, + const char *merchant_url, + const char *instance_id, + const char *scope, + struct GNUNET_TIME_Relative + duration, + bool refreshable, + unsigned int http_status); + +/** + * Set the access token gotten through another CMD + * + * @param label command label. + * @param other_cmd the other command exposing the bearer_token trait. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_set_instance_token (const char *label, + const char *cmd_job_label); + +/** + * Define a "DELETE /private/token" CMD. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the + * POST /instances request. + * @param instance_id the ID of the instance, or NULL + * @param http_status expected HTTP response code. + * @return the command. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_delete_instance_token (const char *label, + const char *merchant_url, + const char *instance_id, + unsigned int http_status); + +/** * Define a "POST /instances" CMD. Comprehensive version. * * @param label command label. @@ -1968,6 +2018,7 @@ TALER_TESTING_cmd_merchant_get_statisticsamount (const char *label, op (reason, const char) \ op (lock_uuid, const char) \ op (auth_token, const char) \ + op (bearer_token, const char) \ op (paths_length, const uint32_t) \ op (payto_length, const uint32_t) \ op (num_planchets, const uint32_t) \ diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -45,44 +45,6 @@ GNUNET_NETWORK_STRUCT_BEGIN /** - * @brief Hash over an order request, used for the idempotency check. - */ -struct TALER_MerchantPostDataHashP -{ - /** - * The authentication hash is a SHA-512 hash code. - */ - struct GNUNET_HashCode hash; -}; - - -/** - * @brief Hash used for client authenticiation. Computed with a - * `struct TALER_MerchantAuthenticationSaltP`. - */ -struct TALER_MerchantAuthenticationHashP -{ - /** - * The authentication hash is a SHA-512 hash code. - * All zeros if authentication is off. - */ - struct GNUNET_HashCode hash; -}; - - -/** - * @brief Salt used for client authenticiation. - */ -struct TALER_MerchantAuthenticationSaltP -{ - /** - * The authentication salt is a 256-bit value. - */ - uint32_t salt[256 / 8 / sizeof(uint32_t)]; /* = 8 */ -}; - - -/** * Format of the data hashed to generate the notification * string whenever the KYC status for an account has * changed. diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am @@ -18,6 +18,7 @@ libtalermerchant_la_SOURCES = \ merchant_api_common.c merchant_api_common.h \ merchant_api_delete_account.c \ merchant_api_delete_instance.c \ + merchant_api_delete_instance_token.c \ merchant_api_delete_order.c \ merchant_api_delete_otp_device.c \ merchant_api_delete_product.c \ @@ -52,6 +53,7 @@ libtalermerchant_la_SOURCES = \ merchant_api_patch_webhook.c \ merchant_api_post_account.c \ merchant_api_post_instance_auth.c \ + merchant_api_post_instance_token.c \ merchant_api_post_instances.c \ merchant_api_post_orders.c \ merchant_api_post_order_abort.c \ diff --git a/src/lib/merchant_api_delete_instance_token.c b/src/lib/merchant_api_delete_instance_token.c @@ -0,0 +1,178 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 2.1, or (at your option) any later version. + + TALER 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with + TALER; see the file COPYING.LGPL. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant_api_delete_instance_token.c + * @brief Implementation of the DELETE /instance/$ID/private/token request of the merchant's HTTP API + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_merchant_service.h" +#include "merchant_api_curl_defaults.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_signatures.h> + + +/** + * Handle for a DELETE /instance/$ID/private/token operation. + */ +struct TALER_MERCHANT_InstanceTokenDeleteHandle +{ + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_InstanceTokenDeleteCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + +}; + + +/** + * Function called when we're done processing the + * HTTP DELETE /instance/$ID/private/token request. + * + * @param cls the `struct TALER_MERCHANT_TokenDeleteHandle` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_delete_token_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_MERCHANT_InstanceTokenDeleteHandle *tdh = cls; + const json_t *json = response; + struct TALER_MERCHANT_HttpResponse tdr = { + .http_status = (unsigned int) response_code, + .reply = json + }; + + tdh->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Got /instances/$ID/private/token response with status code %u\n", + (unsigned int) response_code); + switch (response_code) + { + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_UNAUTHORIZED: + tdr.ec = TALER_JSON_get_error_code (json); + tdr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, merchant says we need to authenticate. */ + break; + case MHD_HTTP_NOT_FOUND: + break; + default: + /* unexpected response code */ + tdr.ec = TALER_JSON_get_error_code (json); + tdr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for DELETE /instance/$ID/private/token\n", + (unsigned int) response_code, + (int) tdr.ec); + break; + } + tdh->cb (tdh->cb_cls, + &tdr); + TALER_MERCHANT_instance_token_delete_cancel (tdh); +} + + +struct TALER_MERCHANT_InstanceTokenDeleteHandle * +TALER_MERCHANT_instance_token_delete ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance_id, + TALER_MERCHANT_InstanceTokenDeleteCallback cb, + void *cb_cls) +{ + struct TALER_MERCHANT_InstanceTokenDeleteHandle *tdh; + + tdh = GNUNET_new (struct TALER_MERCHANT_InstanceTokenDeleteHandle); + tdh->ctx = ctx; + tdh->cb = cb; + tdh->cb_cls = cb_cls; + { + char *path; + + GNUNET_asprintf (&path, + "instances/%s/private/token", + instance_id); + tdh->url = TALER_url_join (backend_url, + path, + NULL); + GNUNET_free (path); + } + if (NULL == tdh->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (tdh); + return NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + tdh->url); + { + CURL *eh; + + eh = TALER_MERCHANT_curl_easy_get_ (tdh->url); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_CUSTOMREQUEST, + MHD_HTTP_METHOD_DELETE)); + tdh->job = GNUNET_CURL_job_add (ctx, + eh, + &handle_delete_token_finished, + tdh); + } + return tdh; +} + + +void +TALER_MERCHANT_instance_token_delete_cancel ( + struct TALER_MERCHANT_InstanceTokenDeleteHandle *tdh) +{ + if (NULL != tdh->job) + GNUNET_CURL_job_cancel (tdh->job); + GNUNET_free (tdh->url); + GNUNET_free (tdh); +} diff --git a/src/lib/merchant_api_get_config.c b/src/lib/merchant_api_get_config.c @@ -34,12 +34,12 @@ * Which version of the Taler protocol is implemented * by this library? Used to determine compatibility. */ -#define MERCHANT_PROTOCOL_CURRENT 18 +#define MERCHANT_PROTOCOL_CURRENT 19 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 6 +#define MERCHANT_PROTOCOL_AGE 7 /** * How many exchanges do we allow at most per merchant? diff --git a/src/lib/merchant_api_post_instance_auth.c b/src/lib/merchant_api_post_instance_auth.c @@ -130,7 +130,7 @@ TALER_MERCHANT_instance_auth_post ( struct GNUNET_CURL_Context *ctx, const char *backend_url, const char *instance_id, - const char *auth_token, + const char *auth_password, TALER_MERCHANT_InstanceAuthPostCallback cb, void *cb_cls) { @@ -167,7 +167,7 @@ TALER_MERCHANT_instance_auth_post ( GNUNET_free (iaph); return NULL; } - if (NULL == auth_token) + if (NULL == auth_password) { req_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("method", @@ -175,22 +175,11 @@ TALER_MERCHANT_instance_auth_post ( } else { - if (0 != strncasecmp (RFC_8959_PREFIX, - auth_token, - strlen (RFC_8959_PREFIX))) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Authentication token must start with `%s'\n", - RFC_8959_PREFIX); - GNUNET_free (iaph->url); - GNUNET_free (iaph); - return NULL; - } req_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("method", "token"), - GNUNET_JSON_pack_string ("token", - auth_token)); + GNUNET_JSON_pack_string ("password", + auth_password)); } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Requesting URL '%s'\n", diff --git a/src/lib/merchant_api_post_instance_token.c b/src/lib/merchant_api_post_instance_token.c @@ -0,0 +1,235 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 2.1, or (at your option) any later version. + + TALER 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with + TALER; see the file COPYING.LGPL. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant_api_post_instance_token.c + * @brief Implementation of the POST /instance/$ID/private/token request + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <curl/curl.h> +#include <jansson.h> +#include <microhttpd.h> /* just for HTTP status codes */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include "taler_merchant_service.h" +#include "merchant_api_curl_defaults.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_signatures.h> +#include <taler/taler_curl_lib.h> + + +/** + * Handle for a POST /instances/$ID/private/token operation. + */ +struct TALER_MERCHANT_InstanceTokenPostHandle +{ + /** + * The url for this request. + */ + char *url; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_MERCHANT_InstanceTokenPostCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + +}; + + +/** + * Function called when we're done processing the + * HTTP GET /instances/$ID/private/token request. + * + * @param cls the `struct TALER_MERCHANT_InstanceTokenPostHandle` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_post_instance_token_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_MERCHANT_InstanceTokenPostHandle *itph = cls; + const json_t *json = response; + struct TALER_MERCHANT_HttpResponse hr = { + .http_status = (unsigned int) response_code, + .reply = json + }; + + itph->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Got /instances/$ID response with status code %u\n", + (unsigned int) response_code); + switch (response_code) + { + case MHD_HTTP_OK: + break; + case MHD_HTTP_BAD_REQUEST: + /* happens if the auth token is malformed */ + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, merchant says we need to authenticate. */ + break; + case MHD_HTTP_UNAUTHORIZED: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + /* Nothing really to verify, merchant says we need to authenticate. */ + break; + default: + /* unexpected response code */ + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d\n", + (unsigned int) response_code, + (int) hr.ec); + break; + } + itph->cb (itph->cb_cls, + &hr); + TALER_MERCHANT_instance_token_post_cancel (itph); +} + + +struct TALER_MERCHANT_InstanceTokenPostHandle * +TALER_MERCHANT_instance_token_post ( + struct GNUNET_CURL_Context *ctx, + const char *backend_url, + const char *instance_id, + const char *scope, + struct GNUNET_TIME_Relative duration, + bool refreshable, + TALER_MERCHANT_InstanceTokenPostCallback cb, + void *cb_cls) +{ + struct TALER_MERCHANT_InstanceTokenPostHandle *itph; + json_t *req_obj; + + itph = GNUNET_new (struct TALER_MERCHANT_InstanceTokenPostHandle); + itph->ctx = ctx; + itph->cb = cb; + itph->cb_cls = cb_cls; + if (NULL != instance_id) + { + char *path; + + GNUNET_asprintf (&path, + "instances/%s/private/token", + instance_id); + itph->url = TALER_url_join (backend_url, + path, + NULL); + GNUNET_free (path); + } + else + { + /* backend_url is already identifying the instance */ + itph->url = TALER_url_join (backend_url, + "private/token", + NULL); + } + if (NULL == itph->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (itph); + return NULL; + } + if (NULL == scope) + { + req_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_time_rel ("duration", + duration), + GNUNET_JSON_pack_bool ("refreshable", + refreshable), + GNUNET_JSON_pack_string ("scope", + "readonly")); + } + else + { + req_obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_time_rel ("duration", + duration), + GNUNET_JSON_pack_bool ("refreshable", + refreshable), + GNUNET_JSON_pack_string ("scope", + scope)); + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + itph->url); + { + CURL *eh; + + eh = TALER_MERCHANT_curl_easy_get_ (itph->url); + if (GNUNET_OK != + TALER_curl_easy_post (&itph->post_ctx, + eh, + req_obj)) + { + GNUNET_break (0); + curl_easy_cleanup (eh); + json_decref (req_obj); + GNUNET_free (itph->url); + GNUNET_free (itph); + return NULL; + } + json_decref (req_obj); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_CUSTOMREQUEST, + MHD_HTTP_METHOD_POST)); + itph->job = GNUNET_CURL_job_add2 (ctx, + eh, + itph->post_ctx.headers, + &handle_post_instance_token_finished, + itph); + } + return itph; +} + + +void +TALER_MERCHANT_instance_token_post_cancel ( + struct TALER_MERCHANT_InstanceTokenPostHandle *itph) +{ + if (NULL != itph->job) + GNUNET_CURL_job_cancel (itph->job); + TALER_curl_easy_post_finished (&itph->post_ctx); + GNUNET_free (itph->url); + GNUNET_free (itph); +} diff --git a/src/lib/merchant_api_post_instances.c b/src/lib/merchant_api_post_instances.c @@ -170,7 +170,7 @@ TALER_MERCHANT_instances_post ( bool use_stefan, struct GNUNET_TIME_Relative default_wire_transfer_delay, struct GNUNET_TIME_Relative default_pay_delay, - const char *auth_token, + const char *auth_password, TALER_MERCHANT_InstancesPostCallback cb, void *cb_cls) { @@ -178,22 +178,13 @@ TALER_MERCHANT_instances_post ( json_t *req_obj; json_t *auth_obj; - if (NULL != auth_token) + if (NULL != auth_password) { - if (0 != strncasecmp (RFC_8959_PREFIX, - auth_token, - strlen (RFC_8959_PREFIX))) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Authentication token must start with `%s'\n", - RFC_8959_PREFIX); - return NULL; - } auth_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("method", "token"), - GNUNET_JSON_pack_string ("token", - auth_token)); + GNUNET_JSON_pack_string ("password", + auth_password)); } else { diff --git a/src/merchant-tools/taler-merchant-passwd.c b/src/merchant-tools/taler-merchant-passwd.c @@ -65,16 +65,6 @@ run (void *cls, global_ret = -1; return; } - if (0 != strncmp (pw, - RFC_8959_PREFIX, - strlen (RFC_8959_PREFIX))) - { - fprintf (stderr, - "Invalid password specified, does not begin with `%s'\n", - RFC_8959_PREFIX); - global_ret = 1; - return; - } if (NULL == instance) instance = GNUNET_strdup ("admin"); cfg = GNUNET_CONFIGURATION_dup (config); @@ -91,17 +81,9 @@ run (void *cls, GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, &ias.auth_salt, sizeof (ias.auth_salt)); - GNUNET_assert (GNUNET_YES == - GNUNET_CRYPTO_kdf (&ias.auth_hash, - sizeof (ias.auth_hash), - &ias.auth_salt, - sizeof (ias.auth_salt), - pw, - strlen (pw), - "merchant-instance-auth", - strlen ("merchant-instance-auth"), - NULL, - 0)); + TALER_merchant_instance_auth_hash_with_salt (&ias.auth_hash, + &ias.auth_salt, + pw); if (GNUNET_OK != plugin->connect (plugin->cls)) { diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am @@ -58,6 +58,7 @@ libtalermerchanttesting_la_SOURCES = \ testing_api_cmd_kyc_get.c \ testing_api_cmd_lock_product.c \ testing_api_cmd_instance_auth.c \ + testing_api_cmd_instance_token.c \ testing_api_cmd_merchant_get_order.c \ testing_api_cmd_patch_instance.c \ testing_api_cmd_patch_otp_device.c \ diff --git a/src/testing/test_merchant_accounts.sh b/src/testing/test_merchant_accounts.sh @@ -41,7 +41,7 @@ echo -n "Configuring 'admin' instance ..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ http://localhost:9966/management/instances \ - -d '{"auth":{"method":"token","token":"secret-token:new_value"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ + -d '{"auth":{"method":"token","password":"secret-token:new_value"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "204" ] diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -1004,7 +1004,7 @@ run (void *cls, "instance-create-i1a-auth-ok", merchant_url, "i1a", - RFC_8959_PREFIX "my-secret", + "my-secret", MHD_HTTP_NO_CONTENT), TALER_TESTING_cmd_merchant_get_product ("get-nx-product-i1a-3", merchant_url_i1a, @@ -1016,7 +1016,20 @@ run (void *cls, MHD_HTTP_UNAUTHORIZED, NULL), TALER_TESTING_cmd_set_authorization ("set-auth-valid", - "Bearer " RFC_8959_PREFIX "my-secret"), + "Basic aTFhOm15LXNlY3JldA=="), + TALER_TESTING_cmd_merchant_post_instance_token ( + "instance-create-i1a-token-ok", + merchant_url, + "i1a", + "write", /* scope */ + GNUNET_TIME_UNIT_DAYS, /* duration */ + GNUNET_YES, /* refreshable */ + MHD_HTTP_OK), + TALER_TESTING_cmd_set_authorization ("unset-auth-valid", + NULL), // Unset header + TALER_TESTING_cmd_merchant_set_instance_token ( + "instance-create-i1a-token-set", + "instance-create-i1a-token-ok"), TALER_TESTING_cmd_merchant_get_product ("get-nx-product-i1a-4", merchant_url_i1a, "nx-product", @@ -1026,11 +1039,10 @@ run (void *cls, merchant_url_i1a, MHD_HTTP_OK, NULL), - TALER_TESTING_cmd_merchant_post_instance_auth ( - "instance-create-i1a-change-auth", - merchant_url_i1a, - NULL, - RFC_8959_PREFIX "my-other-secret", + TALER_TESTING_cmd_merchant_delete_instance_token ( + "instance-create-i1a-token-delete", + merchant_url, + "i1a", MHD_HTTP_NO_CONTENT), TALER_TESTING_cmd_merchant_get_product ("get-nx-product-i1a-5", merchant_url_i1a, @@ -1045,9 +1057,21 @@ run (void *cls, merchant_url_i1a, NULL, MHD_HTTP_UNAUTHORIZED), - TALER_TESTING_cmd_set_authorization ( + TALER_TESTING_cmd_set_authorization ("set-token-auth-valid-again", + "Basic aTFhOm15LXNlY3JldA=="), + TALER_TESTING_cmd_merchant_post_instance_token ( "set-auth-valid-again", - "Bearer " RFC_8959_PREFIX "my-other-secret"), + merchant_url, + "i1a", + "write", /* scope */ + GNUNET_TIME_UNIT_DAYS, /* duration */ + GNUNET_YES, /* refreshable */ + MHD_HTTP_OK), + TALER_TESTING_cmd_set_authorization ("unset-auth-valid2", + NULL), // Unset header + TALER_TESTING_cmd_merchant_set_instance_token ( + "instance-create-i1a-token-set-again", + "set-auth-valid-again"), TALER_TESTING_cmd_merchant_post_instance_auth ( "instance-create-i1a-auth-ok-idempotent", merchant_url_i1a, diff --git a/src/testing/test_merchant_instance_auth.sh b/src/testing/test_merchant_instance_auth.sh @@ -41,7 +41,7 @@ echo -n "Configuring 'admin' instance ..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ http://localhost:9966/management/instances \ - -d '{"auth":{"method":"token","token":"secret-token:new_value"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ + -d '{"auth":{"method":"token","password":"new_pw"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "204" ] @@ -49,8 +49,27 @@ then exit_fail "Expected 204, instance created. got: $STATUS" >&2 fi + +BASIC_AUTH=$(echo -n admin:new_pw | base64) + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H "Authorization: Basic $BASIC_AUTH" \ + http://localhost:9966/private/token \ + -d '{"scope":"write"}' \ + -w "%{http_code}" -s -o $LAST_RESPONSE) + + +if [ "$STATUS" != "200" ] +then + exit_fail "Expected 200 OK. Got: $STATUS" +fi + +TOKEN=$(jq -e -r .access_token < $LAST_RESPONSE) + +echo " OK" >&2 + STATUS=$(curl -H "Content-Type: application/json" -X POST \ - -H 'Authorization: Bearer secret-token:new_value' \ + -H "Authorization: Bearer $TOKEN" \ http://localhost:9966/private/accounts \ -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/43?receiver-name=user43"}' \ -w "%{http_code}" -s -o /dev/null) @@ -73,14 +92,18 @@ setup -c test_template.conf \ -u "exchange-account-2" \ -r "merchant-exchange-default" -NEW_SECRET=secret-token:different_value +NEW_SECRET="different_value" taler-merchant-exchangekeyupdate \ -c "${CONF}" \ -L DEBUG \ 2> taler-merchant-exchangekeyupdate2.log & +taler-merchant-passwd \ + -c "${CONF}" \ + -L DEBUG \ + "$NEW_SECRET" \ + 2> taler-merchant-passwd.log taler-merchant-httpd \ - -a "${NEW_SECRET}" \ -c "${CONF}" \ -L DEBUG \ 2> taler-merchant-httpd2.log & @@ -110,11 +133,28 @@ then exit_fail "Failed to (re)start merchant backend" fi +echo " OK" >&2 + +BASIC_AUTH=$(echo -n "admin:$NEW_SECRET" | base64) + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H "Authorization: Basic $BASIC_AUTH" \ + http://localhost:9966/private/token \ + -d '{"scope":"write"}' \ + -w "%{http_code}" -s -o $LAST_RESPONSE) + + +if [ "$STATUS" != "200" ] +then + exit_fail "Expected 200 OK. Got: $STATUS" +fi + +TOKEN=$(jq -e -r .access_token < $LAST_RESPONSE) echo -n "Creating order to test auth is ok..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ 'http://localhost:9966/private/orders' \ - -H 'Authorization: Bearer '"$NEW_SECRET" \ + -H 'Authorization: Bearer '"$TOKEN" \ -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"}}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") @@ -125,10 +165,10 @@ then fi ORDER_ID=$(jq -e -r .order_id < "$LAST_RESPONSE") -TOKEN=$(jq -e -r .token < "$LAST_RESPONSE") +ORD_TOKEN=$(jq -e -r .token < "$LAST_RESPONSE") STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \ - -H 'Authorization: Bearer '"$NEW_SECRET" \ + -H 'Authorization: Bearer '"$TOKEN" \ -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] @@ -139,14 +179,29 @@ fi PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE") -echo "OK order ${ORDER_ID} with ${TOKEN} and ${PAY_URL}" >&2 +echo "OK order ${ORDER_ID} with ${ORD_TOKEN} and ${PAY_URL}" >&2 echo -n "Configuring 'second' instance ..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ - -H 'Authorization: Bearer '"$NEW_SECRET" \ + -H 'Authorization: Bearer '"$TOKEN" \ + http://localhost:9966/management/instances \ + -d '{"auth":{"method":"token","password":"second"},"id":"second","name":"second","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ + -w "%{http_code}" -s -o /dev/null) + +if [ "$STATUS" != "204" ] +then + exit_fail "Expected 204, instance created. got: $STATUS" +fi + +echo "OK" >&2 + +echo -n "Configuring 'third' instance ..." >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Bearer '"$TOKEN" \ http://localhost:9966/management/instances \ - -d '{"auth":{"method":"token","token":"secret-token:second"},"id":"second","name":"second","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ + -d '{"auth":{"method":"token","password":"third"},"id":"third","name":"third","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "204" ] @@ -159,23 +214,24 @@ echo "OK" >&2 echo -n "Updating 'second' instance token using the 'new_one' auth token..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ - -H 'Authorization: Bearer '"$NEW_SECRET" \ + -H 'Authorization: Bearer '"$TOKEN" \ http://localhost:9966/management/instances/second/auth \ - -d '{"method":"token","token":"secret-token:new_one"}' \ + -d '{"method":"token","password":"new_one"}' \ -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "204" ] then exit_fail "Expected 204, instance auth token changed. got: $STATUS" fi -NEW_SECRET="secret-token:new_one" +NEW_SECRET="new_one" echo " OK" >&2 +BASIC_AUTH2=$(echo -n second:$NEW_SECRET | base64) echo -n "Requesting login token..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ - -H 'Authorization: Bearer '"$NEW_SECRET" \ + -H 'Authorization: Basic '"$BASIC_AUTH2" \ http://localhost:9966/instances/second/private/token \ -d '{"scope":"readonly","refreshable":true}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") @@ -186,7 +242,25 @@ then exit_fail "Expected 200, login token created. got: $STATUS" fi -TOKEN=$(jq -e -r .token < "$LAST_RESPONSE") +TOKEN=$(jq -e -r .access_token < "$LAST_RESPONSE") + +echo " OK" >&2 + +echo -n "Requesting login token... (write)" >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Basic '"$BASIC_AUTH2" \ + http://localhost:9966/instances/second/private/token \ + -d '{"scope":"write","refreshable":true}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq < "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, login token created. got: $STATUS" +fi + +RWTOKEN=$(jq -e -r .access_token < "$LAST_RESPONSE") echo " OK" >&2 @@ -204,6 +278,37 @@ fi echo " OK" >&2 +echo -n "Updating 'second' instance token using the 'second' auth token..." >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Bearer '"$RWTOKEN" \ + http://localhost:9966/instances/second/private/auth \ + -d '{"method":"token","password":"again"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "204" ] +then + cat $LAST_RESPONSE + exit_fail "Expected 204, instance not authorized. got: $STATUS" +fi + +echo " OK" >&2 + +echo -n "Updating 'third' instance token using the 'second' auth token..." >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Bearer '"$RWTOKEN" \ + http://localhost:9966/management/instances/third/auth \ + -d '{"method":"token","password":"new_one"}' \ + -w "%{http_code}" -s -o /dev/null) + +if [ "$STATUS" != "401" ] +then + exit_fail "Expected 401, instance not authorized. got: $STATUS" +fi + +echo " OK" >&2 + echo -n "Refreshing login token..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ diff --git a/src/testing/test_merchant_instance_creation.sh b/src/testing/test_merchant_instance_creation.sh @@ -27,7 +27,7 @@ echo -n "Configuring a merchant instance before configuring the admin instance . STATUS=$(curl -H "Content-Type: application/json" -X POST \ http://localhost:9966/management/instances \ - -d '{"auth":{"method":"token","token":"secret-token:other_secret"},"id":"first","name":"test","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ + -d '{"auth":{"method":"token","password":"secret-token:other_secret"},"id":"first","name":"test","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "204" ] @@ -58,7 +58,7 @@ echo -n "Configuring a second merchant instance ..." STATUS=$(curl -H "Content-Type: application/json" -X POST \ http://localhost:9966/management/instances \ - -d '{"auth":{"method":"token","token":"secret-token:other_secret"},"id":"second","name":"test","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ + -d '{"auth":{"method":"token","password":"secret-token:other_secret"},"id":"second","name":"test","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "401" ] diff --git a/src/testing/test_merchant_instance_purge.sh b/src/testing/test_merchant_instance_purge.sh @@ -21,6 +21,7 @@ # Launch only the merchant. setup -c test_template.conf -m +LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX) echo -n "Configuring admin instance ..." >&2 @@ -41,7 +42,7 @@ echo -n "Configuring merchant instance ..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ http://localhost:9966/management/instances \ - -d '{"auth":{"method":"token","token":"secret-token:other_secret"},"id":"test","name":"test","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ + -d '{"auth":{"method":"token","password":"secret-token:other_secret"},"id":"test","name":"test","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "204" ] diff --git a/src/testing/test_merchant_instance_response.sh b/src/testing/test_merchant_instance_response.sh @@ -22,6 +22,7 @@ # Launch only the merchant. setup -c test_template.conf -m +LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX) STATUS=$(curl -H "Content-Type: application/json" -X OPTIONS \ http://localhost:9966/private/products \ @@ -33,7 +34,6 @@ then fi STATUS=$(curl -H "Content-Type: application/json" -X GET \ - -H 'Authorization: Bearer secret-token:super_secret' \ http://localhost:9966/private/products \ -w "%{http_code}" -s -o /dev/null) @@ -43,9 +43,8 @@ then fi STATUS=$(curl -H "Content-Type: application/json" -X POST \ - -H 'Authorization: Bearer secret-token:super_secret' \ http://localhost:9966/management/instances \ - -d '{"auth":{"method":"token","token":"secret-token:other_secret"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ + -d '{"auth":{"method":"token","password":"other_secret"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "204" ] @@ -62,8 +61,24 @@ then exit_fail "Expected 401 without the token for the list of product when the admin instance was created. got: $STATUS" fi +BASIC_AUTH=$(echo -n admin:other_secret | base64) + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H "Authorization: Basic $BASIC_AUTH" \ + http://localhost:9966/private/token \ + -d '{"scope":"write"}' \ + -w "%{http_code}" -s -o $LAST_RESPONSE) + + +if [ "$STATUS" != "200" ] +then + exit_fail "Expected 200 OK. Got: $STATUS" +fi + +TOKEN=$(jq -e -r .access_token < $LAST_RESPONSE) + STATUS=$(curl -H "Content-Type: application/json" -X GET \ - -H 'Authorization: Bearer secret-token:other_secret' \ + -H "Authorization: Bearer $TOKEN" \ http://localhost:9966/private/products \ -w "%{http_code}" -s -o /dev/null) @@ -73,9 +88,9 @@ then fi STATUS=$(curl -H "Content-Type: application/json" -X POST \ - -H 'Authorization: Bearer secret-token:other_secret' \ + -H "Authorization: Bearer $TOKEN" \ http://localhost:9966/private/auth \ - -d '{"method":"token","token":"secret-token:zxc"}' \ + -d '{"method":"token","password":"zxc"}' \ -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "204" ] @@ -94,18 +109,40 @@ then exit_fail "Expected 401 without the token, when purging the instance. got: $STATUS" fi -STATUS=$(curl -H "Content-Type: application/json" -X DELETE \ - -H 'Authorization: Bearer secret-token:other_secret' \ - "http://localhost:9966/private" \ - -w "%{http_code}" -s -o /dev/null) +# FIXME: what we probably want here is that when changing the instance authentication +# settings all tokens are invalidated. We would have to add another DB operation +# for that. For now, we simply check here that we cannot get a new token with the +# old password. +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H "Authorization: Basic $BASIC_AUTH" \ + http://localhost:9966/private/token \ + -d '{"scope":"write"}' \ + -w "%{http_code}" -s -o $LAST_RESPONSE) + if [ "$STATUS" != "401" ] then - exit_fail "Expected 401 using old token, when purging the instance. got: $STATUS" + exit_fail "Expected 401 with old password. Got: $STATUS" fi +BASIC_AUTH=$(echo -n admin:zxc | base64) + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H "Authorization: Basic $BASIC_AUTH" \ + http://localhost:9966/private/token \ + -d '{"scope":"write"}' \ + -w "%{http_code}" -s -o $LAST_RESPONSE) + + +if [ "$STATUS" != "200" ] +then + exit_fail "Expected 200 OK. Got: $STATUS" +fi + +TOKEN=$(jq -e -r .access_token < $LAST_RESPONSE) + STATUS=$(curl -H "Content-Type: application/json" -X DELETE \ - -H 'Authorization: Bearer secret-token:zxc' \ + -H "Authorization: Bearer $TOKEN" \ "http://localhost:9966/private" \ -w "%{http_code}" -s -o /dev/null) @@ -115,7 +152,7 @@ then fi STATUS=$(curl -H "Content-Type: application/json" -X GET \ - -H 'Authorization: Bearer secret-token:zxc' \ + -H "Authorization: Bearer $TOKEN" \ http://localhost:9966/private/products \ -w "%{http_code}" -s -o /dev/null) diff --git a/src/testing/test_merchant_product_creation.sh b/src/testing/test_merchant_product_creation.sh @@ -52,8 +52,6 @@ setup -c "test_template.conf" \ -em \ $BANK_FLAGS -bash - LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX) WALLET_DB=$(mktemp -p "${TMPDIR:-/tmp}" test_wallet.json-XXXXXX) CONF="test_template.conf.edited" diff --git a/src/testing/testing_api_cmd_instance_token.c b/src/testing/testing_api_cmd_instance_token.c @@ -0,0 +1,394 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + TALER 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 General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing_api_cmd_instance_token.c + * @brief command to test /private/token POSTing + * @author Martin Schanzenbach + */ +#include "platform.h" +#include <taler/taler_exchange_service.h> +#include <taler/taler_testing_lib.h> +#include "taler_merchant_service.h" +#include "taler_merchant_testing_lib.h" + + +/** + * State of a "POST /instances/$ID/private/token" CMD. + */ +struct TokenInstanceState +{ + + /** + * Handle for a "POST token" request. + */ + struct TALER_MERCHANT_InstanceTokenPostHandle *itph; + + /** + * Handle for a "DELETE token" request. + */ + struct TALER_MERCHANT_InstanceTokenDeleteHandle *itdh; + + /** + * The interpreter state. + */ + struct TALER_TESTING_Interpreter *is; + + /** + * Base URL of the merchant serving the request. + */ + const char *merchant_url; + + /** + * ID of the instance to run GET for. + */ + const char *instance_id; + + /** + * The received token (if any). + */ + char *token; + + /** + * Desired scope. Can be NULL + */ + const char *scope; + + /** + * Desired duration. + */ + struct GNUNET_TIME_Relative duration; + + /** + * Refreshable? + */ + bool refreshable; + + /** + * Expected HTTP response code. + */ + unsigned int http_status; + + /** + * DELETE or POST. + */ + unsigned int is_delete; + +}; + +/** + * Callback for a POST /instances/$ID/private/token operation. + * + * @param cls closure for this function + * @param hr response being processed + */ +static void +token_instance_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr) +{ + struct TokenInstanceState *tis = cls; + const char *scope; + struct GNUNET_TIME_Timestamp duration; + bool refreshable; + const char *error_name; + unsigned int error_line; + + + tis->itph = NULL; + if (tis->http_status != hr->http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u (%d) to command %s\n", + hr->http_status, + (int) hr->ec, + TALER_TESTING_interpreter_get_current_label (tis->is)); + TALER_TESTING_interpreter_fail (tis->is); + return; + } + switch (hr->http_status) + { + case MHD_HTTP_NO_CONTENT: + GNUNET_assert (GNUNET_YES == tis->is_delete); + break; + case MHD_HTTP_OK: + { + /* Get token */ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string_copy ("access_token", + &tis->token), + GNUNET_JSON_spec_string ("scope", + &scope), + GNUNET_JSON_spec_bool ("refreshable", + &refreshable), + GNUNET_JSON_spec_timestamp ("expiration", + &duration), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (GNUNET_NO == tis->is_delete); + if (GNUNET_OK != + GNUNET_JSON_parse (hr->reply, + spec, + &error_name, + &error_line)) + { + char *js; + + js = json_dumps (hr->reply, + JSON_INDENT (1)); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Parser failed on %s:%u for input `%s'\n", + error_name, + error_line, + js); + free (js); + TALER_TESTING_FAIL (tis->is); + } + break; + } + case MHD_HTTP_BAD_REQUEST: + /* likely invalid auth value, we do not check client-side */ + break; + case MHD_HTTP_FORBIDDEN: + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unhandled HTTP status %u (%d) returned from /private/token operation.\n", + hr->http_status, + hr->ec); + } + + + TALER_TESTING_interpreter_next (tis->is); +} + + +/** + * set a token + * + * + * @param cls closure. + * @param cmd command being run now. + * @param is interpreter state. + */ +static void +set_token_instance_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + const char *token_job_label = cls; + const char *token; + const struct TALER_TESTING_Command *tok_cmd; + struct GNUNET_CURL_Context *cctx; + char *authorization; + + cctx = TALER_TESTING_interpreter_get_context (is); + GNUNET_assert (NULL != cctx); + tok_cmd = TALER_TESTING_interpreter_lookup_command ( + is, + token_job_label); + TALER_TESTING_get_trait_bearer_token (tok_cmd, + &token); + GNUNET_assert (NULL != token); + + GNUNET_asprintf (&authorization, + "%s: Bearer %s", + MHD_HTTP_HEADER_AUTHORIZATION, + token); + GNUNET_assert (GNUNET_OK == + GNUNET_CURL_append_header (cctx, + authorization)); + GNUNET_free (authorization); + TALER_TESTING_interpreter_next (is); +} + + +/** + * Run the "token /instances/$ID" CMD. + * + * + * @param cls closure. + * @param cmd command being run now. + * @param is interpreter state. + */ +static void +token_instance_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct TokenInstanceState *tis = cls; + + tis->is = is; + if (GNUNET_NO == tis->is_delete) + tis->itph = TALER_MERCHANT_instance_token_post ( + TALER_TESTING_interpreter_get_context (is), + tis->merchant_url, + tis->instance_id, + tis->scope, + tis->duration, + tis->refreshable, + &token_instance_cb, + tis); + else + tis->itdh = TALER_MERCHANT_instance_token_delete ( + TALER_TESTING_interpreter_get_context (is), + tis->merchant_url, + tis->instance_id, + &token_instance_cb, + tis); + GNUNET_assert ((NULL != tis->itph) || (NULL != tis->itdh)); +} + + +/** + * Free the state of a "POST instance token" CMD, and possibly + * cancel a pending operation thereof. + * + * @param cls closure. + * @param cmd command being run. + */ +static void +token_instance_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct TokenInstanceState *tis = cls; + + if (NULL != tis->itph) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "%s /instance/$ID/token operation did not complete\n", + (GNUNET_NO == tis->is_delete) ? "DELETE" : "POST"); + if (GNUNET_NO == tis->is_delete) + TALER_MERCHANT_instance_token_post_cancel (tis->itph); + else + TALER_MERCHANT_instance_token_delete_cancel (tis->itdh); + } + GNUNET_free (tis); +} + + +/** + * Offer internal data to other commands. + * + * @param cls closure + * @param[out] ret result (could be anything) + * @param trait name of the trait + * @param index index number of the object to extract. + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +token_instance_traits (void *cls, + const void **ret, + const char *trait, + unsigned int index) +{ + struct TokenInstanceState *ais = cls; + struct TALER_TESTING_Trait traits[] = { + TALER_TESTING_make_trait_bearer_token (ais->token), + TALER_TESTING_trait_end () + }; + + return TALER_TESTING_get_trait (traits, + ret, + trait, + index); +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_delete_instance_token (const char *label, + const char *merchant_url, + const char *instance_id, + unsigned int http_status) +{ + struct TokenInstanceState *tis; + + tis = GNUNET_new (struct TokenInstanceState); + tis->merchant_url = merchant_url; + tis->instance_id = instance_id; + tis->is_delete = GNUNET_YES; + tis->http_status = http_status; + + { + struct TALER_TESTING_Command cmd = { + .cls = tis, + .label = label, + .run = &token_instance_run, + .cleanup = &token_instance_cleanup, + .traits = &token_instance_traits + }; + + return cmd; + } +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_set_instance_token (const char *label, + const char *token_job_label) +{ + { + struct TALER_TESTING_Command cmd = { + .cls = (void*) token_job_label, // FIXME scope + .label = label, + .run = &set_token_instance_run, + .cleanup = NULL, + .traits = NULL + }; + + return cmd; + } +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_instance_token (const char *label, + const char *merchant_url, + const char *instance_id, + const char *scope, + struct GNUNET_TIME_Relative + duration, + bool refreshable, + unsigned int http_status) +{ + struct TokenInstanceState *tis; + + tis = GNUNET_new (struct TokenInstanceState); + tis->merchant_url = merchant_url; + tis->instance_id = instance_id; + tis->scope = scope; + tis->duration = duration; + tis->refreshable = refreshable; + tis->is_delete = GNUNET_NO; + tis->http_status = http_status; + + { + struct TALER_TESTING_Command cmd = { + .cls = tis, + .label = label, + .run = &token_instance_run, + .cleanup = &token_instance_cleanup, + .traits = &token_instance_traits + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_token_instance.c */ diff --git a/src/testing/testing_api_cmd_pay_order.c b/src/testing/testing_api_cmd_pay_order.c @@ -546,7 +546,7 @@ build_coins (struct TALER_MERCHANT_PayCoin **pc, const struct TALER_DenominationSignature *denom_sig; const struct TALER_Amount *denom_value; const struct TALER_EXCHANGE_DenomPublicKey *denom_pub; - const struct TALER_AgeCommitmentHash *h_age_commitment; + const struct TALER_AgeCommitmentHashP *h_age_commitment; GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_coin_priv (coin_cmd,