Files
core/test/rust/native/application/auth.rs
T
2026-11-03 03:32:07 +00:00

391 lines
11 KiB
Rust

use super::{
http::{Server, request, request_head},
*,
};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use hmac::{Hmac, Mac};
use sha2::Sha256;
mod jobs;
mod metadata;
mod rest;
mod rest_json;
mod roles;
mod tasks;
use std::{
net::{TcpListener, TcpStream},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
pub(super) const KEY: &str = "native-test-key-only-not-for-deployment";
fn token(subject: &str, session: &str) -> String {
tenant_token(subject, session, None)
}
pub(super) fn tenant_token(subject: &str, session: &str, tenant: Option<&str>) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let claims = serde_json::json!({"iss":"session","aud":"admin","sub":subject,"sid":session,"tenant":tenant,"iat":now-1,"exp":now+59});
let signed = format!(
"{}.{}",
URL_SAFE_NO_PAD.encode(r#"{"alg":"HS256","typ":"JWT"}"#),
URL_SAFE_NO_PAD.encode(claims.to_string())
);
let mut mac = Hmac::<Sha256>::new_from_slice(KEY.as_bytes()).unwrap();
mac.update(signed.as_bytes());
format!(
"{signed}.{}",
URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes())
)
}
#[test]
fn authenticated_http_executes_dever_verify_and_observes_revocation() {
let directory = Directory::new();
verify_authentication(
&directory,
serde_json::json!({"type":"sqlite","path":"data/account.db","max_connections":4}),
);
}
#[cfg(feature = "postgres")]
#[test]
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
fn postgres_authenticated_http_executes_dever_verify_and_observes_revocation() {
let directory = Directory::new();
let schema = super::database::postgres::Schema::new(
&directory.0,
"dever_native_auth",
"CREATE SCHEMA dever_native_auth",
"DROP SCHEMA dever_native_auth CASCADE",
);
let mut connection = schema.connection.clone();
connection["max_connections"] = 4.into();
verify_authentication(&directory, connection);
}
// 相同业务源码覆盖两种驱动的登录、角色、REST 和入队后撤销会话。
fn verify_authentication(directory: &Directory, connection: serde_json::Value) {
let project = Project::load(&fixture().with_file_name("auth")).unwrap();
let program = project.check().unwrap();
assert_eq!(program.verifiers.len(), 1);
let binary = build::build(
&program,
&project.sources,
"main",
&directory.0.join("auth"),
)
.unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
std::fs::create_dir(directory.0.join("config")).unwrap();
let settings = serde_json::json!({
"http":{"host":"127.0.0.1","port":port},
"sites":{"admin":{"path":"admin","auth":"session","hosts":["fixture.test"],"origin":"https://fixture.test"}},
"database":{"default":connection},
"runtime":{"mode":"api"},
"auth":{"providers":{"session":{"verify":"demo.account.verify","jwtSecret":KEY,"ttlSeconds":60,"cookie":"session"}}}
});
std::fs::write(
directory.0.join("config/setting.json"),
settings.to_string(),
)
.unwrap();
let seeded = execute(
&binary,
"demo.account.seed",
r#"{"subject":"user","session":"valid-session"}"#,
);
assert!(
seeded.status.success(),
"{}",
String::from_utf8_lossy(&seeded.stderr)
);
let mut server = Server(
Command::new(&binary)
.stdout(std::process::Stdio::null())
.spawn()
.unwrap(),
);
let deadline = Instant::now() + Duration::from_secs(5);
while TcpStream::connect(("127.0.0.1", port)).is_err() {
assert!(server.0.try_wait().unwrap().is_none());
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(10));
}
let path = "/demo/account/admin/session/me";
metadata::check(port);
let (status, body) = request(
port,
"POST",
"/demo/account/admin/secret/probe",
"fixture.test",
"Content-Type: application/json\r\n",
r#"{"input":{"key":"fixture-only-key"}}"#,
);
assert_eq!(status, 200, "{body}");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&body).unwrap()["data"],
32
);
let health = "/demo/account/admin/session/health";
let login = "/demo/account/admin/session/login";
let origin = "Origin: https://fixture.test\r\nContent-Type: application/json\r\n";
let credentials = r#"{"subject":"user","session":"valid-session"}"#;
assert_eq!(
request(
port,
"POST",
login,
"fixture.test",
"Content-Type: application/json\r\n",
credentials
)
.0,
403
);
let (status, headers, _) =
request_head(port, "POST", login, "fixture.test", origin, credentials);
assert_eq!(status, 200);
let set_cookie = headers
.lines()
.find_map(|line| line.strip_prefix("set-cookie: "))
.unwrap();
assert!(set_cookie.contains("; Secure; HttpOnly; SameSite=Lax; Max-Age=60"));
let issued = format!("Cookie: {}\r\n", set_cookie.split(';').next().unwrap());
assert_eq!(
request(port, "GET", path, "fixture.test", &issued, "").0,
200
);
let (status, headers, _) = request_head(
port,
"POST",
"/demo/account/admin/session/login_fail",
"fixture.test",
origin,
credentials,
);
assert_eq!(status, 409);
assert!(!headers.to_ascii_lowercase().contains("set-cookie"));
let (status, headers, _) = request_head(
port,
"POST",
"/demo/account/admin/session/logout",
"fixture.test",
origin,
"{}",
);
assert_eq!(status, 200);
assert!(headers.contains("session=; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=0"));
assert_eq!(request(port, "GET", path, "fixture.test", "", "").0, 401);
assert_eq!(
request(
port,
"GET",
&format!("{path}?extra=1"),
"fixture.test",
"",
""
)
.0,
401
);
assert_eq!(
request(
port,
"GET",
health,
"fixture.test",
"Cookie: session=expired\r\n",
""
)
.0,
200
);
assert_eq!(
request(
port,
"GET",
health,
"fixture.test",
"Authorization: Bearer invalid\r\n",
""
)
.0,
401
);
let bearer = format!(
"Authorization: Bearer {}\r\n",
token("user", "valid-session")
);
let cookie = format!(
"Cookie: session=v1.{}\r\n",
URL_SAFE_NO_PAD.encode(token("user", "valid-session"))
);
for credentials in [&bearer, &cookie] {
let (status, body) = request(port, "GET", path, "fixture.test", credentials, "");
assert_eq!(status, 200, "{body}");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&body).unwrap()["data"],
serde_json::json!({"id":"user", "session":"valid-session", "site":"admin", "user_id":1, "tenant_id":null})
);
}
assert_eq!(
request(
port,
"GET",
&format!("{path}?extra=1"),
"fixture.test",
&bearer,
""
)
.0,
400
);
assert_eq!(
request(
port,
"GET",
path,
"fixture.test",
&format!("{bearer}{cookie}"),
""
)
.0,
401
);
assert_eq!(
request(
port,
"GET",
path,
"fixture.test",
&format!(
"Authorization: Bearer {}\r\n",
token("user", "revoked-session")
),
""
)
.0,
401
);
assert_eq!(
request(
port,
"POST",
path,
"fixture.test",
&format!("{cookie}Content-Type: application/json\r\n"),
"{}"
)
.0,
403
);
assert_eq!(
request(
port,
"POST",
path,
"fixture.test",
&format!("{cookie}Origin: https://fixture.test\r\nContent-Type: application/json\r\n"),
"{}"
)
.0,
200
);
let expired = execute(
&binary,
"demo.account.set_deadline",
r#"{"id":1,"expires":0}"#,
);
assert!(
expired.status.success(),
"{}",
String::from_utf8_lossy(&expired.stderr)
);
assert_eq!(
request(port, "GET", path, "fixture.test", &bearer, "").0,
401
);
let renewed = execute(
&binary,
"demo.account.set_deadline",
r#"{"id":1,"expires":9223372036854775807}"#,
);
assert!(
renewed.status.success(),
"{}",
String::from_utf8_lossy(&renewed.stderr)
);
assert_eq!(
request(port, "GET", path, "fixture.test", &bearer, "").0,
200
);
roles::check(&binary, port, &bearer);
rest::check(&binary, port, &bearer);
rest_json::check(port, &bearer);
let job_count = jobs::before_revocation(&binary, port, &bearer, &settings);
let revoked = execute(&binary, "demo.account.revoke", r#"{"id":1}"#);
assert!(revoked.status.success());
jobs::after_revocation(&binary, &settings, job_count);
assert_eq!(
request(port, "GET", path, "fixture.test", &bearer, "").0,
401
);
assert_eq!(
request(port, "GET", health, "fixture.test", &cookie, "").0,
200
);
assert_eq!(
request(port, "GET", health, "fixture.test", &bearer, "").0,
401
);
assert!(
Command::new("sh")
.args([
"-c",
"kill -TERM \"$1\"",
"dever-test",
&server.0.id().to_string()
])
.status()
.unwrap()
.success()
);
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if let Some(status) = server.0.try_wait().unwrap() {
assert!(status.success());
break;
}
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(10));
}
let mut cross_pool = settings.clone();
cross_pool["database"]["demo"] =
serde_json::json!({"type":"sqlite","path":"data/other.db","max_connections":4});
std::fs::write(
directory.0.join("config/setting.json"),
cross_pool.to_string(),
)
.unwrap();
let rejected = Command::new(&binary).output().unwrap();
assert!(!rejected.status.success());
assert!(
String::from_utf8_lossy(&rejected.stderr)
.contains("transaction cannot span configured database pools"),
"{}",
String::from_utf8_lossy(&rejected.stderr)
);
assert!(TcpStream::connect(("127.0.0.1", port)).is_err());
let mut invalid = settings;
invalid["auth"]["providers"]["session"]["verify"] = "demo.account.unknown".into();
std::fs::write(directory.0.join("config/setting.json"), invalid.to_string()).unwrap();
let invalid = Command::new(&binary).output().unwrap();
assert!(!invalid.status.success());
assert!(String::from_utf8_lossy(&invalid.stderr).contains("uncompiled verify"));
}