1 parent
cfc74d9db6
commit
dacdf9c27d
76 files changed
+5209
-741
No files matched your search
+236
-30
@@ -12,7 +12,8 @@ use tower_governor::key_extractor::KeyExtractor;
|
||||
|
||||
use crate::imap::client::mock::MockImapClient;
|
||||
use crate::imap::client::{
|
||||
EmailAddress, ImapAttachment, ImapError, ImapFolder, ImapMessageBody, ImapMessageHeader,
|
||||
EmailAddress, FolderStatusExtended, ImapAttachment, ImapError, ImapFolder, ImapMessageBody,
|
||||
ImapMessageHeader,
|
||||
};
|
||||
use crate::smtp::client::SmtpError;
|
||||
use crate::smtp::client::mock::MockSmtpClient;
|
||||
@@ -304,15 +305,18 @@ async fn disabled_optional_features_are_not_registered() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_policy_exposes_optional_feature_switches() {
|
||||
async fn auth_policy_exposes_only_login_requirements_before_authentication() {
|
||||
let dir = setup_static_dir();
|
||||
let mut config = (*test_config(dir.path().to_str().unwrap())).clone();
|
||||
config.remember_me_enabled = true;
|
||||
config.sieve_enabled = false;
|
||||
config.diagnostics_enabled = false;
|
||||
config.about_enabled = false;
|
||||
let store = test_store();
|
||||
let (browser_id, account_id, token) = setup_test_account(&store, "alice@example.com");
|
||||
let app = create_router(
|
||||
Arc::new(config),
|
||||
test_store(),
|
||||
store,
|
||||
test_imap_client(),
|
||||
test_smtp_client(),
|
||||
test_search_engine("/tmp/oxi-test"),
|
||||
@@ -321,6 +325,7 @@ async fn auth_policy_exposes_optional_feature_switches() {
|
||||
);
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/auth/policy")
|
||||
@@ -332,13 +337,52 @@ async fn auth_policy_exposes_optional_feature_switches() {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(json["rememberMeEnabled"], true);
|
||||
for key in [
|
||||
"rememberMeDays",
|
||||
"sessionTimeoutHours",
|
||||
"pgpEnabled",
|
||||
"sieveEnabled",
|
||||
"diagnosticsEnabled",
|
||||
"aboutEnabled",
|
||||
] {
|
||||
assert!(json.get(key).is_none(), "public policy leaked {key}");
|
||||
}
|
||||
|
||||
let unauth = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/auth/session-policy")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(unauth.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
let mut request = Request::builder()
|
||||
.uri("/api/auth/session-policy")
|
||||
.header("x-requested-with", "XMLHttpRequest");
|
||||
for (name, value) in auth_headers(&browser_id, &account_id, &token) {
|
||||
request = request.header(name, value);
|
||||
}
|
||||
let response = app
|
||||
.oneshot(request.body(Body::empty()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(json["sieveEnabled"], false);
|
||||
assert_eq!(json["diagnosticsEnabled"], false);
|
||||
assert_eq!(json["aboutEnabled"], false);
|
||||
assert!(json.get("rememberMeDays").is_none());
|
||||
assert!(json.get("sessionTimeoutHours").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn security_headers_are_opt_in() {
|
||||
async fn security_headers_are_opt_in_and_content_type_aware() {
|
||||
let dir = setup_static_dir();
|
||||
let app = create_router(
|
||||
test_config(dir.path().to_str().unwrap()),
|
||||
@@ -350,43 +394,134 @@ async fn security_headers_are_opt_in() {
|
||||
test_idle_manager(),
|
||||
);
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/health")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(response.headers().get("content-security-policy").is_none());
|
||||
|
||||
let mut config = (*test_config(dir.path().to_str().unwrap())).clone();
|
||||
config.security_headers_enabled = true;
|
||||
let app = create_router(
|
||||
Arc::new(config),
|
||||
assert!(response.headers().get("x-content-type-options").is_none());
|
||||
let disabled_api = create_router(
|
||||
test_config(dir.path().to_str().unwrap()),
|
||||
test_store(),
|
||||
test_imap_client(),
|
||||
test_smtp_client(),
|
||||
test_search_engine("/tmp/oxi-test"),
|
||||
test_event_bus(),
|
||||
test_idle_manager(),
|
||||
);
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/health")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.headers()["x-frame-options"], "DENY");
|
||||
)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/health")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
response.headers()["content-security-policy"]
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains("default-src 'self'")
|
||||
disabled_api
|
||||
.headers()
|
||||
.get("x-content-type-options")
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
disabled_api
|
||||
.headers()
|
||||
.get("cross-origin-resource-policy")
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
disabled_api
|
||||
.headers()
|
||||
.get("content-security-policy")
|
||||
.is_none()
|
||||
);
|
||||
assert!(disabled_api.headers().get("x-frame-options").is_none());
|
||||
|
||||
let mut config = (*test_config(dir.path().to_str().unwrap())).clone();
|
||||
config.security_headers_enabled = true;
|
||||
let config = Arc::new(config);
|
||||
|
||||
let html = create_router(
|
||||
config.clone(),
|
||||
test_store(),
|
||||
test_imap_client(),
|
||||
test_smtp_client(),
|
||||
test_search_engine("/tmp/oxi-test"),
|
||||
test_event_bus(),
|
||||
test_idle_manager(),
|
||||
)
|
||||
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(html.headers()["x-content-type-options"], "nosniff");
|
||||
assert_eq!(html.headers()["x-frame-options"], "DENY");
|
||||
assert_eq!(html.headers()["referrer-policy"], "no-referrer");
|
||||
assert_eq!(
|
||||
html.headers()["permissions-policy"],
|
||||
"camera=(), microphone=(), geolocation=()"
|
||||
);
|
||||
assert_eq!(html.headers()["cross-origin-opener-policy"], "same-origin");
|
||||
let html_csp = html.headers()["content-security-policy"].to_str().unwrap();
|
||||
assert!(html_csp.contains("default-src 'self'"));
|
||||
assert!(html_csp.contains("script-src 'self';"));
|
||||
assert!(html_csp.contains("script-src-attr 'none'"));
|
||||
assert!(html_csp.contains("style-src 'self'; style-src-attr 'unsafe-inline'"));
|
||||
assert!(!html_csp.contains("style-src 'self' 'unsafe-inline'"));
|
||||
assert!(!html_csp.contains("unsafe-inline'; script-src"));
|
||||
assert!(!html_csp.contains("script-src 'self' 'unsafe-inline'"));
|
||||
|
||||
let css = create_router(
|
||||
config.clone(),
|
||||
test_store(),
|
||||
test_imap_client(),
|
||||
test_smtp_client(),
|
||||
test_search_engine("/tmp/oxi-test"),
|
||||
test_event_bus(),
|
||||
test_idle_manager(),
|
||||
)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/assets/app.css")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(css.headers()["x-content-type-options"], "nosniff");
|
||||
assert_eq!(css.headers()["referrer-policy"], "no-referrer");
|
||||
assert_eq!(css.headers()["cross-origin-resource-policy"], "same-origin");
|
||||
assert!(css.headers().get("x-frame-options").is_none());
|
||||
assert!(css.headers().get("permissions-policy").is_none());
|
||||
assert!(css.headers().get("cross-origin-opener-policy").is_none());
|
||||
assert!(css.headers().get("content-security-policy").is_none());
|
||||
|
||||
let api = create_router(
|
||||
config,
|
||||
test_store(),
|
||||
test_imap_client(),
|
||||
test_smtp_client(),
|
||||
test_search_engine("/tmp/oxi-test"),
|
||||
test_event_bus(),
|
||||
test_idle_manager(),
|
||||
)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/health")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(api.headers()["x-content-type-options"], "nosniff");
|
||||
assert_eq!(api.headers()["cache-control"], "no-store");
|
||||
assert_eq!(api.headers()["cross-origin-resource-policy"], "same-origin");
|
||||
assert_eq!(
|
||||
api.headers()["content-security-policy"],
|
||||
"frame-ancestors 'none'"
|
||||
);
|
||||
assert_eq!(api.headers()["x-frame-options"], "DENY");
|
||||
assert!(api.headers().get("referrer-policy").is_none());
|
||||
assert!(api.headers().get("permissions-policy").is_none());
|
||||
assert!(api.headers().get("cross-origin-opener-policy").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -945,6 +1080,77 @@ async fn get_folders_returns_200_with_folder_list() {
|
||||
assert_eq!(folders[1]["name"], "Sent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn folder_refresh_uses_server_counts_without_relisting_fresh_folder_tree() {
|
||||
let static_dir = setup_static_dir();
|
||||
let data_dir = TempDir::new().unwrap();
|
||||
let config = test_config_with_imap(
|
||||
static_dir.path().to_str().unwrap(),
|
||||
data_dir.path().to_str().unwrap(),
|
||||
);
|
||||
let store = test_store();
|
||||
let (browser_id, account_id, token) = setup_test_account(&store, "alice@example.com");
|
||||
let user_hash = crate::auth::user_data::hash_email("alice@example.com");
|
||||
provision_user_db(data_dir.path().to_str().unwrap(), &user_hash);
|
||||
|
||||
let conn = crate::db::pool::open_user_db(data_dir.path().to_str().unwrap(), &user_hash)
|
||||
.expect("open user DB");
|
||||
crate::db::folders::upsert_folder(
|
||||
&conn,
|
||||
"INBOX",
|
||||
Some("/"),
|
||||
None,
|
||||
"\\HasNoChildren",
|
||||
true,
|
||||
3444,
|
||||
1,
|
||||
1,
|
||||
10,
|
||||
)
|
||||
.unwrap();
|
||||
crate::db::folders::touch_all_folders(&conn).unwrap();
|
||||
drop(conn);
|
||||
|
||||
let mock = MockImapClient::new().with_folder_status_extended(FolderStatusExtended {
|
||||
uid_validity: 1,
|
||||
exists: 3444,
|
||||
uid_next: 3445,
|
||||
unseen: 12,
|
||||
highest_modseq: 11,
|
||||
});
|
||||
let app = create_router(
|
||||
config,
|
||||
store,
|
||||
Arc::new(mock),
|
||||
test_smtp_client(),
|
||||
test_search_engine(data_dir.path().to_str().unwrap()),
|
||||
test_event_bus(),
|
||||
test_idle_manager(),
|
||||
);
|
||||
|
||||
let mut request = Request::builder()
|
||||
.uri("/api/folders?refresh=true")
|
||||
.header("x-requested-with", "XMLHttpRequest");
|
||||
for (name, value) in auth_headers(&browser_id, &account_id, &token) {
|
||||
request = request.header(name, value);
|
||||
}
|
||||
let response = app
|
||||
.oneshot(request.body(Body::empty()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
let inbox = json["folders"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|folder| folder["name"] == "INBOX")
|
||||
.expect("INBOX remains in the cached folder tree");
|
||||
assert_eq!(inbox["total_count"], 3444);
|
||||
assert_eq!(inbox["unread_count"], 12);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_folders_returns_503_when_imap_fails() {
|
||||
let static_dir = setup_static_dir();
|
||||
|
||||
Reference in new issue
Block a user