Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,390 @@
|
||||
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"));
|
||||
}
|
||||
Reference in new issue
Block a user