merchant

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

commit 0a27ab1fd54e3ad0465975eda0322c82f3843cf2
parent d09dacf70f2ff28a7730dd770ad3ceff0b94caa1
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Tue, 17 Jun 2025 15:58:24 +0200

Rework scopes and access capabilities, Issue #9647

Diffstat:
Msrc/backend/taler-merchant-httpd.c | 24+++++++++++++++++++-----
Msrc/backend/taler-merchant-httpd.h | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/backend/taler-merchant-httpd_helper.c | 2+-
Msrc/backend/taler-merchant-httpd_private-post-instances-ID-token.c | 54++++++++++++++++++++++++++++++++++++++++--------------
Msrc/testing/test_merchant_instance_auth.sh | 7++++---
Msrc/testing/test_merchant_instance_response.sh | 4++--
6 files changed, 134 insertions(+), 34 deletions(-)

diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -205,6 +205,18 @@ static const struct GNUNET_CONFIGURATION_Handle *cfg; */ char *TMH_default_pass; +static bool +capabilities_in_scope (enum TMH_AuthScope scope, enum TMH_AuthCapability + required_capas) +{ + uint64_t diff_capas; + + /* Get capas that differ between required and scoped */ + diff_capas = scope ^ required_capas; + /* Check if differing capas are all covered by scoped capabilities */ + return diff_capas & scope; +} + enum GNUNET_GenericReturnValue TMH_check_auth (const char *token, @@ -251,7 +263,7 @@ TMH_check_auth_instance (const char *token, char *tmp; const char *instance_name; const char *password; - const char *target_instance = "default"; + const char *target_instance = "admin"; enum GNUNET_GenericReturnValue ret; /* implicitly a zeroed out hash means no authentication */ @@ -1289,7 +1301,7 @@ url_handler (void *cls, /* POST /token: */ { .url_prefix = "/token", - .auth_scope = TMH_AS_REFRESHABLE, + .ac_required = TMH_AC_REFRESHABLE, .method = MHD_HTTP_METHOD_POST, .handler = &TMH_private_post_instances_ID_token, /* Body should be tiny. */ @@ -1298,7 +1310,7 @@ url_handler (void *cls, /* DELETE /token: */ { .url_prefix = "/token", - .auth_scope = TMH_AS_READ_ONLY, + .ac_required = TMH_AC_ALWAYS_READ, .method = MHD_HTTP_METHOD_DELETE, .handler = &TMH_private_delete_instances_ID_token, }, @@ -1956,6 +1968,8 @@ url_handler (void *cls, hc->auth_scope = TMH_AS_ALL; else hc->auth_scope = TMH_AS_NONE; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Token auth scope %u\n", hc-> + auth_scope); } } else /* Check bearer token */ @@ -2014,8 +2028,8 @@ url_handler (void *cls, - 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_AC_NONE != hc->rh->ac_required) && + (capabilities_in_scope (hc->auth_scope, hc->rh->ac_required)) ) || ( (TMH_AS_READ_ONLY == (hc->auth_scope & TMH_AS_READ_ONLY)) && (0 == strcmp (MHD_HTTP_METHOD_GET, method)) ) ) ) diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -408,6 +408,49 @@ struct TMH_HandlerContext; /** + * Access capabilities. This is a bit mask. + */ +enum TMH_AuthCapability +{ + /** + * Nothing is allowed. + */ + TMH_AC_NONE = 0, + + /** + * Token renewal is OK. + */ + TMH_AC_REFRESHABLE = 1, + + /** + * Read-only access is OK. Any GET request is + * automatically OK. + */ + TMH_AC_ALWAYS_READ = 2, + + /** + * Order creation and payment status check only + */ + TMH_AC_ORDER_CREATION = 4, + + /** + * Payment status check only + */ + TMH_AC_PAYMENT_STATUS = 8, + + /** + * Inventory locking, + */ + TMH_AC_INVENTORY_LOCKING = 16, + + /** + * Order creation and refund + */ + TMH_AC_REFUND = 32, + +}; + +/** * Possible authorization scopes. This is a bit mask. */ enum TMH_AuthScope @@ -415,23 +458,41 @@ enum TMH_AuthScope /** * Nothing is authorized. */ - TMH_AS_NONE = 0, + TMH_AS_NONE = TMH_AC_NONE, /** * Read-only access is OK. Any GET request is * automatically OK. */ - TMH_AS_READ_ONLY = 1, + TMH_AS_READ_ONLY = TMH_AC_ALWAYS_READ, /** - * /login access to renew the token is OK. + * Order creation and payment status check only */ - TMH_AS_REFRESHABLE = 2, + TMH_AS_ORDER_SIMPLE = TMH_AC_ORDER_CREATION | TMH_AC_PAYMENT_STATUS, + + /** + * Order creation and inventory locking, + * includes #TMH_AS_ORDER_SIMPLE + */ + TMH_AS_ORDER_POS = TMH_AS_ORDER_SIMPLE | TMH_AC_INVENTORY_LOCKING, + + /** + * Order creation and refund + */ + TMH_AS_ORDER_MGMT = TMH_AS_ORDER_SIMPLE | TMH_AC_REFUND, + + /** + * Order full + * Includes #TMH_ORDER_POS and #TMH_ORDER_MGMT + */ + TMH_AS_ORDER_FULL = TMH_AS_ORDER_POS | TMH_AS_ORDER_MGMT, /** * Full access is granted to everything. */ - TMH_AS_ALL = 7 + TMH_AS_ALL = UINT32_MAX, + }; @@ -456,11 +517,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 capabilities for this request. */ - enum TMH_AuthScope auth_scope; + enum TMH_AuthCapability ac_required; /** * Does this request include an identifier segment diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c @@ -568,7 +568,7 @@ TMH_check_token (const char *token, instance_id, &btoken, &expiration, - &scope); + (uint32_t*) &scope); if (qs < 0) { GNUNET_break (0); 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,49 @@ 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; + if (0 == strcasecmp (scope_prefix, + "readonly")) + iscope = TMH_AS_READ_ONLY; + else if (0 == strcasecmp (scope_prefix, + "write")) + iscope = TMH_AS_ALL; + else if (0 == strcasecmp (scope_prefix, + "order-simple")) + iscope = TMH_AS_ORDER_SIMPLE; + else if (0 == strcasecmp (scope_prefix, + "order-pos")) + iscope = TMH_AS_ORDER_POS; + else if (0 == strcasecmp (scope_prefix, + "order-mgmt")) + iscope = TMH_AS_ORDER_MGMT; + else if (0 == strcasecmp (scope_prefix, + "order-full")) + iscope = TMH_AS_ORDER_FULL; + else + { + 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))) + iscope |= TMH_AC_REFRESHABLE; + if ((TMH_AS_ALL != hc->auth_scope) && + (0 != (iscope & (~hc->auth_scope)))) { /* more permissions requested for the new token, not allowed */ GNUNET_break_op (0); diff --git a/src/testing/test_merchant_instance_auth.sh b/src/testing/test_merchant_instance_auth.sh @@ -50,7 +50,7 @@ then fi -BASIC_AUTH=$(echo -n default:new_pw | base64) +BASIC_AUTH=$(echo -n admin:new_pw | base64) STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H "Authorization: Basic $BASIC_AUTH" \ @@ -131,7 +131,7 @@ fi echo " OK" >&2 -BASIC_AUTH=$(echo -n default:$NEW_SECRET | base64) +BASIC_AUTH=$(echo -n admin:$NEW_SECRET | base64) STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H "Authorization: Basic $BASIC_AUTH" \ @@ -280,10 +280,11 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H 'Authorization: Bearer '"$RWTOKEN" \ http://localhost:9966/instances/second/private/auth \ -d '{"method":"password","password":"again"}' \ - -w "%{http_code}" -s -o /dev/null) + -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] then + cat $LAST_RESPONSE exit_fail "Expected 204, instance not authorized. got: $STATUS" fi diff --git a/src/testing/test_merchant_instance_response.sh b/src/testing/test_merchant_instance_response.sh @@ -61,7 +61,7 @@ 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 default:other_secret | base64) +BASIC_AUTH=$(echo -n admin:other_secret | base64) STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H "Authorization: Basic $BASIC_AUTH" \ @@ -125,7 +125,7 @@ then exit_fail "Expected 401 with old password. Got: $STATUS" fi -BASIC_AUTH=$(echo -n default:zxc | base64) +BASIC_AUTH=$(echo -n admin:zxc | base64) STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H "Authorization: Basic $BASIC_AUTH" \