391 lines
11 KiB
Rust
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"));
|
|
}
|