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:
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,