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
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.
238 lines
9.7 KiB
Rust
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");
|
|
}
|