fix(billing): new users get zero quota by default not unlimited
A freshly registered user's auto-provisioned default downstream key must start with a zero spend allowance, not unlimited. Two parts: - DEFAULT_NEW_USER_MAX_COST_USD changes from 1.0 to 0.0, so new users have no free spend until they top up or an admin raises the cap. - The constant's docs now spell out the failure mode it guards: a NULL max_cost is treated as *unlimited* by the proxy, so the column must always be bound on INSERT. The registration path (routes::auth::register) and the admin create-user path (routes::admin::users) both already include the max_cost column and bind this constant, so both now persist 0.0 rather than leaving NULL. Tests (cargo test --lib routes::auth::tests, 10 passed): assert the constant is 0.0, and run the registration key INSERT against an in-memory DB to verify the stored max_cost is non-NULL and exactly 0.0. The agent suites (approval/sandbox/bash_sandbox/toctou) stay green.
This commit is contained in:
+58
-4
@@ -19,10 +19,12 @@ use serde_json::{json, Value};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Default spending cap (USD) granted to a newly created user's auto-generated
|
||||
/// API key. Per the billing design (新用户默认 $1 额度), new users start with a
|
||||
/// $1 allowance on their default downstream key.
|
||||
pub const DEFAULT_NEW_USER_MAX_COST_USD: f64 = 1.0;
|
||||
/// Default spending cap (USD) on a newly created user's auto-generated default
|
||||
/// API key. Set to `0.0` so a freshly registered user has **zero** spend
|
||||
/// allowance until they top up or an admin raises the cap. This must always be
|
||||
/// bound explicitly on INSERT: a NULL `max_cost` is treated as *unlimited* by
|
||||
/// the proxy, so leaving it unbound silently grants free unlimited usage.
|
||||
pub const DEFAULT_NEW_USER_MAX_COST_USD: f64 = 0.0;
|
||||
|
||||
// ── Request / Response types ─────────────────────────────────────────
|
||||
|
||||
@@ -1417,4 +1419,56 @@ mod tests {
|
||||
let (_, _, message, _, _) = latest_audit_event(&state).await;
|
||||
assert!(message.contains("IP=203.0.113.9"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_user_default_quota_is_zero() {
|
||||
// New users must start with zero spend allowance, not unlimited.
|
||||
assert_eq!(DEFAULT_NEW_USER_MAX_COST_USD, 0.0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn registration_default_key_has_non_null_zero_max_cost() {
|
||||
// Regression guard for the billing leak: the default downstream key
|
||||
// created at registration must persist max_cost = 0.0, never NULL
|
||||
// (NULL is treated as unlimited by the proxy). This runs the exact
|
||||
// INSERT the register handler uses.
|
||||
let state = test_state().await;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'user')",
|
||||
)
|
||||
.bind("quota-user")
|
||||
.bind("quota@example.test")
|
||||
.bind("hash")
|
||||
.execute(&*state.db)
|
||||
.await
|
||||
.expect("insert user");
|
||||
let user_id = sqlx::query_scalar::<_, i64>("SELECT id FROM users WHERE email = ?")
|
||||
.bind("quota@example.test")
|
||||
.fetch_one(&*state.db)
|
||||
.await
|
||||
.expect("user id");
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO downstream_api_keys (name, key, user_id, enabled, max_cost) \
|
||||
VALUES (?, ?, ?, 1, ?)",
|
||||
)
|
||||
.bind("quota-user 的默认密钥")
|
||||
.bind("sk-user-quota-test")
|
||||
.bind(user_id)
|
||||
.bind(DEFAULT_NEW_USER_MAX_COST_USD)
|
||||
.execute(&*state.db)
|
||||
.await
|
||||
.expect("insert default key");
|
||||
|
||||
// max_cost must be present (non-NULL) and exactly zero.
|
||||
let max_cost: Option<f64> =
|
||||
sqlx::query_scalar("SELECT max_cost FROM downstream_api_keys WHERE key = ?")
|
||||
.bind("sk-user-quota-test")
|
||||
.fetch_one(&*state.db)
|
||||
.await
|
||||
.expect("select max_cost");
|
||||
assert!(max_cost.is_some(), "max_cost must not be NULL (NULL = unlimited)");
|
||||
assert_eq!(max_cost.unwrap(), 0.0, "new user default key must have zero quota");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user