250 lines
7.9 KiB
Rust
250 lines
7.9 KiB
Rust
#[path = "support/temp.rs"]
|
|
mod temp;
|
|
|
|
use std::fs;
|
|
|
|
use dever_runtime::api;
|
|
use dever_runtime::auth::{self, Error, Identity};
|
|
use dever_runtime::bytes::Bytes;
|
|
use dever_runtime::config::Settings;
|
|
use dever_runtime::http::{Header, Request};
|
|
|
|
fn settings() -> (temp::TemporaryDirectory, Settings) {
|
|
let root = temp::TemporaryDirectory::new();
|
|
fs::create_dir(root.path().join("config")).unwrap();
|
|
fs::write(root.path().join("config/setting.json"), r#"{
|
|
"auth": {
|
|
"providers": {
|
|
"session": {
|
|
"verify": "user.account.verify",
|
|
"jwtSecret": "0123456789abcdef0123456789abcdef",
|
|
"ttlSeconds": 3600,
|
|
"cookie": "cms_session"
|
|
}
|
|
}
|
|
},
|
|
"sites": {
|
|
"admin": {"path": "admin", "auth": "session", "hosts": ["cms.example.com"], "origin": "https://cms.example.com"},
|
|
"front": {"path": "front", "auth": "session", "hosts": ["cms.example.com"], "origin": "https://cms.example.com"}
|
|
}
|
|
}"#).unwrap();
|
|
let settings = Settings::load_project(root.path()).unwrap();
|
|
(root, settings)
|
|
}
|
|
|
|
fn request(method: &str, extra_headers: Vec<Header>) -> Request {
|
|
let mut headers = vec![Header {
|
|
name: "host".into(),
|
|
value: Bytes::from_text("cms.example.com"),
|
|
}];
|
|
headers.extend(extra_headers);
|
|
Request {
|
|
method: method.into(),
|
|
target: "/news/article/profile".into(),
|
|
headers,
|
|
body: Bytes::from_text(""),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn site_scope_issues_and_verifies_a_bound_identity() {
|
|
let (_root, settings) = settings();
|
|
let login_request = request(
|
|
"POST",
|
|
vec![Header {
|
|
name: "origin".into(),
|
|
value: Bytes::from_text("https://cms.example.com"),
|
|
}],
|
|
);
|
|
let login_settings = &settings;
|
|
let response = api::scoped_request(login_request, move |request| async move {
|
|
let prepared = auth::prepare_with_settings(login_settings, &["front"], &request, true)
|
|
.map_err(|error| error.to_string())?;
|
|
auth::scope(prepared, None, async {
|
|
auth::issue_cookie("account:7", "session:19", Some("tenant:3"))?;
|
|
api::commit_response_metadata()?;
|
|
Ok(api::success(dever_runtime::wire::Encoded::null()))
|
|
})
|
|
.await
|
|
.map_err(|error| error.to_string())?
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let cookie = response
|
|
.headers
|
|
.iter()
|
|
.find(|header| header.name == "set-cookie")
|
|
.map(|header| {
|
|
std::str::from_utf8(header.value.values())
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_owned()
|
|
})
|
|
.expect("issued session cookie");
|
|
let authenticated = request(
|
|
"GET",
|
|
vec![Header {
|
|
name: "cookie".into(),
|
|
value: Bytes::from_string(cookie.clone()),
|
|
}],
|
|
);
|
|
let prepared =
|
|
auth::prepare_with_settings(&settings, &["front", "profile"], &authenticated, false)
|
|
.unwrap();
|
|
let claims = prepared.claims().unwrap();
|
|
assert_eq!(claims.subject, "account:7");
|
|
assert_eq!(claims.session, "session:19");
|
|
assert_eq!(claims.tenant.as_deref(), Some("tenant:3"));
|
|
assert_eq!(claims.site, "front");
|
|
|
|
let identity = Identity {
|
|
id: "account:7".into(),
|
|
user_id: Some(7),
|
|
tenant_id: Some(3),
|
|
};
|
|
auth::scope(prepared, Some(identity), async {
|
|
assert_eq!(auth::site_key().unwrap(), "front");
|
|
assert_eq!(auth::id().unwrap(), "account:7");
|
|
assert_eq!(auth::session().unwrap(), "session:19");
|
|
assert_eq!(auth::user_id().unwrap(), Some(7));
|
|
assert_eq!(auth::tenant_id().unwrap(), Some(3));
|
|
#[cfg(feature = "sqlite")]
|
|
dever_runtime::job::bind_permission("news.article.front.read").unwrap();
|
|
#[cfg(feature = "sqlite")]
|
|
assert!(matches!(
|
|
dever_runtime::job::execution_identity().unwrap(),
|
|
dever_runtime::job::ExecutionIdentity::User { permission_key, .. }
|
|
if permission_key == "news.article.front.read"
|
|
));
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let cross_site = request(
|
|
"GET",
|
|
vec![Header {
|
|
name: "cookie".into(),
|
|
value: Bytes::from_string(cookie),
|
|
}],
|
|
);
|
|
assert_eq!(
|
|
auth::prepare_with_settings(&settings, &["admin"], &cross_site, false)
|
|
.err()
|
|
.expect("site-bound token must fail"),
|
|
Error::Unauthorized
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn public_routes_reject_bad_credentials_and_cookie_writes_require_origin() {
|
|
let (_root, settings) = settings();
|
|
let invalid = request(
|
|
"GET",
|
|
vec![Header {
|
|
name: "authorization".into(),
|
|
value: Bytes::from_text("Bearer invalid"),
|
|
}],
|
|
);
|
|
assert_eq!(
|
|
auth::prepare_with_settings(&settings, &["front"], &invalid, true)
|
|
.err()
|
|
.expect("invalid credential must not become anonymous"),
|
|
Error::Unauthorized
|
|
);
|
|
|
|
let cookie_write = request(
|
|
"DELETE",
|
|
vec![Header {
|
|
name: "cookie".into(),
|
|
value: Bytes::from_text("cms_session=v1.aW52YWxpZA"),
|
|
}],
|
|
);
|
|
assert_eq!(
|
|
auth::prepare_with_settings(&settings, &["front"], &cookie_write, false)
|
|
.err()
|
|
.expect("unsafe cookie request requires Origin"),
|
|
Error::Forbidden
|
|
);
|
|
|
|
let stale_cookie = request(
|
|
"POST",
|
|
vec![Header {
|
|
name: "cookie".into(),
|
|
value: Bytes::from_text("cms_session=v1.invalid"),
|
|
}],
|
|
);
|
|
let prepared = auth::prepare_with_settings(&settings, &["front"], &stale_cookie, true)
|
|
.expect("public login must ignore stale Cookie credentials");
|
|
assert!(prepared.claims().is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unsafe_cookie_writes_require_the_configured_external_origin() {
|
|
let (_root, settings) = settings();
|
|
for origin in [
|
|
None,
|
|
Some("http://cms.example.com"),
|
|
Some("https://cms.example.com:444"),
|
|
Some("https://other.example.com"),
|
|
] {
|
|
let headers = origin
|
|
.into_iter()
|
|
.map(|value| Header {
|
|
name: "origin".into(),
|
|
value: Bytes::from_text(value),
|
|
})
|
|
.collect();
|
|
let request = request("POST", headers);
|
|
let test_settings = &settings;
|
|
let response = api::scoped_request(request, |request| async move {
|
|
let prepared = auth::prepare_with_settings(test_settings, &["front"], &request, true)
|
|
.map_err(|error| error.to_string())?;
|
|
auth::scope(prepared, None, async {
|
|
assert!(auth::issue_cookie("account:7", "session:19", Some("tenant:3")).is_err());
|
|
Ok(api::success(dever_runtime::wire::Encoded::null()))
|
|
})
|
|
.await
|
|
.map_err(|error| error.to_string())?
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert!(
|
|
!response
|
|
.headers
|
|
.iter()
|
|
.any(|header| header.name == "set-cookie")
|
|
);
|
|
}
|
|
|
|
let valid = request(
|
|
"POST",
|
|
vec![Header {
|
|
name: "origin".into(),
|
|
value: Bytes::from_text("https://cms.example.com"),
|
|
}],
|
|
);
|
|
let test_settings = &settings;
|
|
let response = api::scoped_request(valid, |request| async move {
|
|
let prepared = auth::prepare_with_settings(test_settings, &["front"], &request, true)
|
|
.map_err(|error| error.to_string())?;
|
|
auth::scope(prepared, None, async {
|
|
auth::issue_cookie("account:7", "session:19", Some("tenant:3"))?;
|
|
api::commit_response_metadata()?;
|
|
Ok(api::success(dever_runtime::wire::Encoded::null()))
|
|
})
|
|
.await
|
|
.map_err(|error| error.to_string())?
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert!(
|
|
response
|
|
.headers
|
|
.iter()
|
|
.any(|header| header.name == "set-cookie")
|
|
);
|
|
}
|