Files
zclaw_openfang/crates/zclaw-saas/tests/account_security_test.rs
iven c37c7218c2
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
test(saas): add 36 security/validation/permission tests (184 total, 0 failures)
New test files:
- auth_security_test.rs (12): account lockout DB state, lockout reset,
  password version invalidation, disabled account, refresh token
  revocation, boundary validation (username/password), role enforcement,
  TOTP 2FA flow
- account_security_test.rs (9): role management, privilege escalation
  prevention, account disable/enable, cross-account access control,
  operation logs
- relay_validation_test.rs (8): input validation (missing fields, empty
  messages, invalid roles), disabled provider, model listing, task
  isolation
- permission_matrix_test.rs (7): super_admin full access, user allowed/
  forbidden endpoints, public endpoints, unauthenticated rejection,
  API token lifecycle

Discovered: account lockout runtime check broken — handlers.rs:213
parse_from_rfc3339 fails on PostgreSQL TIMESTAMPTZ::TEXT format,
silently skipping lockout. DB state is correct but login not rejected.
2026-04-10 08:11:02 +08:00

238 lines
9.7 KiB
Rust

mod common;
use axum::http::StatusCode;
use common::*;
// ═══════════════════════════════════════════════════════════════════
// Role management: admin can change another user's role
// ═══════════════════════════════════════════════════════════════════
#[tokio::test]
async fn admin_can_change_user_role_to_admin() {
let (app, pool) = build_test_app().await;
let admin = admin_token(&app, &pool, "rolechangeadmin").await;
let user_token = register_token(&app, "targetroleuser").await;
// Get user's account ID
let (_, me) = send(&app, get("/api/v1/auth/me", &user_token)).await;
let user_id = me["id"].as_str().unwrap();
// Admin promotes user to admin
let (status, body) = send(
&app,
patch(
&format!("/api/v1/accounts/{user_id}"),
&admin,
serde_json::json!({ "role": "admin" }),
),
).await;
assert_eq!(status, StatusCode::OK, "admin should be able to change role: {body}");
assert_eq!(body["role"], "admin");
}
#[tokio::test]
async fn admin_can_demote_user_role() {
let (app, pool) = build_test_app().await;
let admin = admin_token(&app, &pool, "demoteadmin").await;
// Create a second admin
let second_admin = admin_token(&app, &pool, "secondadmin").await;
let (_, me) = send(&app, get("/api/v1/auth/me", &second_admin)).await;
let second_id = me["id"].as_str().unwrap();
// Demote to user
let (status, body) = send(
&app,
patch(
&format!("/api/v1/accounts/{second_id}"),
&admin,
serde_json::json!({ "role": "user" }),
),
).await;
assert_eq!(status, StatusCode::OK, "admin should be able to demote: {body}");
assert_eq!(body["role"], "user");
}
// ═══════════════════════════════════════════════════════════════════
// Security: user cannot escalate own role via self-update
// ═══════════════════════════════════════════════════════════════════
#[tokio::test]
async fn user_cannot_escalate_role_via_self_update() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "escalateuser").await;
let (_, me) = send(&app, get("/api/v1/auth/me", &token)).await;
let user_id = me["id"].as_str().unwrap();
// Try to escalate role to admin
let (status, body) = send(
&app,
patch(
&format!("/api/v1/accounts/{user_id}"),
&token,
serde_json::json!({ "role": "admin", "display_name": "Updated" }),
),
).await;
// The update may succeed but role should be ignored
if status == StatusCode::OK {
// Verify role was NOT changed
let (_, updated) = send(&app, get(&format!("/api/v1/accounts/{user_id}"), &token)).await;
assert_ne!(updated["role"], "admin", "user should NOT be able to escalate own role: {updated}");
assert_eq!(updated["display_name"], "Updated", "display_name should still update");
}
// If the endpoint rejects the role field entirely, that's also acceptable
}
// ═══════════════════════════════════════════════════════════════════
// Account status management: admin can disable/enable
// ═══════════════════════════════════════════════════════════════════
#[tokio::test]
async fn admin_can_disable_account() {
let (app, pool) = build_test_app().await;
let admin = admin_token(&app, &pool, "disableadmin").await;
let user_token = register_token(&app, "disabletarget").await;
let (_, me) = send(&app, get("/api/v1/auth/me", &user_token)).await;
let user_id = me["id"].as_str().unwrap();
// Admin disables user
let (status, body) = send(
&app,
patch(
&format!("/api/v1/accounts/{user_id}/status"),
&admin,
serde_json::json!({ "status": "disabled" }),
),
).await;
assert_eq!(status, StatusCode::OK, "admin should be able to disable account: {body}");
// Disabled user cannot log in
let (status, _) = send(
&app,
post_public(
"/api/v1/auth/login",
serde_json::json!({ "username": "disabletarget", "password": DEFAULT_PASSWORD }),
),
).await;
assert_eq!(status, StatusCode::FORBIDDEN, "disabled account should not be able to log in");
}
#[tokio::test]
async fn admin_can_reenable_account() {
let (app, pool) = build_test_app().await;
let admin = admin_token(&app, &pool, "reenableadmin").await;
register(&app, "reenabletarget", "reenable@test.io", DEFAULT_PASSWORD).await;
// Disable first
let (_, me) = send(&app, get("/api/v1/auth/me", &admin)).await;
// Get the target user's ID from accounts list
let (_, list) = send(&app, get("/api/v1/accounts", &admin)).await;
let items = list["items"].as_array().expect("accounts list");
let target_id = items.iter()
.find(|a| a["username"] == "reenabletarget")
.map(|a| a["id"].as_str().unwrap())
.expect("should find target user");
// Disable
send(
&app,
patch(
&format!("/api/v1/accounts/{target_id}/status"),
&admin,
serde_json::json!({ "status": "disabled" }),
),
).await;
// Re-enable
let (status, _) = send(
&app,
patch(
&format!("/api/v1/accounts/{target_id}/status"),
&admin,
serde_json::json!({ "status": "active" }),
),
).await;
assert_eq!(status, StatusCode::OK, "admin should be able to re-enable account");
// User can log in again
let (status, _) = send(
&app,
post_public(
"/api/v1/auth/login",
serde_json::json!({ "username": "reenabletarget", "password": DEFAULT_PASSWORD }),
),
).await;
assert_eq!(status, StatusCode::OK, "re-enabled account should be able to log in");
}
#[tokio::test]
async fn user_cannot_disable_others() {
let (app, _pool) = build_test_app().await;
let token = register_token(&app, "disableattacker").await;
let victim_token = register_token(&app, "disablevictim").await;
let (_, me) = send(&app, get("/api/v1/auth/me", &victim_token)).await;
let victim_id = me["id"].as_str().unwrap();
let (status, _) = send(
&app,
patch(
&format!("/api/v1/accounts/{victim_id}/status"),
&token,
serde_json::json!({ "status": "disabled" }),
),
).await;
assert_eq!(status, StatusCode::FORBIDDEN, "regular user should not be able to disable others");
}
// ═══════════════════════════════════════════════════════════════════
// Cross-account access control
// ═══════════════════════════════════════════════════════════════════
#[tokio::test]
async fn user_cannot_view_other_account_details() {
let (app, _pool) = build_test_app().await;
let token_a = register_token(&app, "viewerA").await;
let token_b = register_token(&app, "viewerB").await;
let (_, me_b) = send(&app, get("/api/v1/auth/me", &token_b)).await;
let b_id = me_b["id"].as_str().unwrap();
let (status, _) = send(&app, get(&format!("/api/v1/accounts/{b_id}"), &token_a)).await;
assert_eq!(status, StatusCode::FORBIDDEN, "user should not see another user's account details");
}
#[tokio::test]
async fn admin_can_view_any_account() {
let (app, pool) = build_test_app().await;
let admin = admin_token(&app, &pool, "viewadmin").await;
let user_token = register_token(&app, "viewtarget").await;
let (_, me) = send(&app, get("/api/v1/auth/me", &user_token)).await;
let user_id = me["id"].as_str().unwrap();
let (status, body) = send(&app, get(&format!("/api/v1/accounts/{user_id}"), &admin)).await;
assert_eq!(status, StatusCode::OK, "admin should be able to view any account: {body}");
assert_eq!(body["username"], "viewtarget");
}
// ═══════════════════════════════════════════════════════════════════
// Operation logs are admin-only
// ═══════════════════════════════════════════════════════════════════
#[tokio::test]
async fn operation_logs_admin_can_list() {
let (app, pool) = build_test_app().await;
let admin = admin_token(&app, &pool, "logadmin").await;
// Perform an action that generates a log
send(&app, get("/api/v1/accounts", &admin)).await;
let (status, body) = send(&app, get("/api/v1/logs/operations", &admin)).await;
assert_eq!(status, StatusCode::OK, "admin should see operation logs: {body}");
// Should be paginated
assert!(body["items"].is_array() || body.is_array(), "logs should be array or paginated");
}