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:
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" \