feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,249 @@
|
||||
#[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")
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user