Compare commits

...
6 changed files with 126 additions and 1 deletions

No files matched your search

@@ -13,6 +13,8 @@ pub fn add_storyteller(cors: Cors, is_production: bool) -> Cors {
// Storyteller Engine (Production)
.allowed_origin(STORYTELLER_ENGINE.https_link_without_path())
// Storyteller Studio (Production)
// DO NOT REMOVE: shipped ArtCraft desktop builds send this origin.
// See deployed_desktop_client_origin_tests.rs.
.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
// Storyteller.ai (Production)
.allowed_origin(STORYTELLER_API.https_link_without_path())
@@ -7,7 +7,10 @@ use domains::storyteller::STORYTELLER_STUDIO_STAGING;
use domains::storyteller::STORYTELLER_STUDIO_TESTING;
pub fn add_storyteller_studio(cors: Cors, _is_production: bool) -> Cors {
cors.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
cors
// DO NOT REMOVE: shipped ArtCraft desktop builds send this origin.
// See deployed_desktop_client_origin_tests.rs.
.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
.allowed_origin(STORYTELLER_STUDIO_STAGING.https_link_without_path())
.allowed_origin(STORYTELLER_STUDIO_TESTING.https_link_without_path())
.allowed_origin_fn(|origin, _req_head| {
@@ -5,6 +5,8 @@ pub fn add_tauri(cors: Cors, _is_production: bool) -> Cors {
// Tauri Windows
.allowed_origin("http://tauri.localhost")
// Tauri Mac
// DO NOT REMOVE: shipped ArtCraft desktop builds send this origin.
// See deployed_desktop_client_origin_tests.rs.
.allowed_origin("tauri://localhost")
}
@@ -0,0 +1,110 @@
//! DO NOT REMOVE THESE ORIGINS: shipped ArtCraft desktop builds depend on them.
//!
//! Old ArtCraft desktop builds in the wild send one of these `Origin` headers:
//!
//! * `https://studio.storyteller.ai`
//! * `tauri://localhost` (macOS Tauri webview)
//!
//! Those builds can't be updated server-side. If the API stops allowing these
//! origins, those installs break: every credentialed request (login, session,
//! generation) fails CORS in the webview.
//!
//! These tests live in their own module on purpose, apart from the Storyteller
//! and Tauri configs, so reworking or deleting those configs (or their tests)
//! can't silently remove this protection. They use string literals, not
//! `domains` constants, so renaming a constant can't change what they check.
use actix_web::dev::Transform;
use actix_web::http::{header, Method, StatusCode};
use actix_web::test::{self, TestRequest};
use server_environment::ServerEnvironment;
use crate::cors::build_cors_config;
const STORYTELLER_STUDIO_ORIGIN: &str = "https://studio.storyteller.ai";
const TAURI_MAC_ORIGIN: &str = "tauri://localhost";
const ENVIRONMENTS: [ServerEnvironment; 2] = [ServerEnvironment::Production, ServerEnvironment::Development];
mod storyteller_studio_origin {
use super::*;
#[actix_rt::test]
async fn credentialed_requests_are_allowed() {
assert_credentialed_requests_allowed(STORYTELLER_STUDIO_ORIGIN).await;
}
#[actix_rt::test]
async fn preflights_are_allowed() {
assert_preflights_allowed(STORYTELLER_STUDIO_ORIGIN).await;
}
}
mod tauri_mac_origin {
use super::*;
#[actix_rt::test]
async fn credentialed_requests_are_allowed() {
assert_credentialed_requests_allowed(TAURI_MAC_ORIGIN).await;
}
#[actix_rt::test]
async fn preflights_are_allowed() {
assert_preflights_allowed(TAURI_MAC_ORIGIN).await;
}
}
/// Simple requests in every environment and method get the origin echoed
/// back with credentials allowed, which is what the webview requires.
async fn assert_credentialed_requests_allowed(origin: &str) {
for environment in ENVIRONMENTS {
let service = build_cors_config(environment)
.new_transform(test::ok_service())
.await
.unwrap();
for method in [Method::GET, Method::POST, Method::PUT, Method::DELETE] {
let request = TestRequest::default()
.method(method.clone())
.uri("/v1/session")
.insert_header((header::ORIGIN, origin))
.to_srv_request();
let response = test::call_service(&service, request).await;
let context = format!(
"Shipped ArtCraft desktop builds send Origin {} ({:?}, {}). DO NOT REMOVE this origin from the CORS config.",
origin, environment, method);
assert_eq!(response.status(), StatusCode::OK, "{}", context);
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).map(|v| v.to_str().unwrap()), Some(origin), "{}", context);
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_CREDENTIALS).map(|v| v.to_str().unwrap()), Some("true"), "{}", context);
}
}
}
/// Preflights for the methods and headers the desktop app uses are accepted.
async fn assert_preflights_allowed(origin: &str) {
for environment in ENVIRONMENTS {
let service = build_cors_config(environment)
.new_transform(test::ok_service())
.await
.unwrap();
for requested_method in ["GET", "POST", "PUT", "DELETE"] {
let request = TestRequest::default()
.method(Method::OPTIONS)
.uri("/v1/session")
.insert_header((header::ORIGIN, origin))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, requested_method))
.insert_header((header::ACCESS_CONTROL_REQUEST_HEADERS, "content-type,session"))
.to_srv_request();
let response = test::call_service(&service, request).await;
let context = format!(
"Shipped ArtCraft desktop builds send Origin {} ({:?}, preflight {}). DO NOT REMOVE this origin from the CORS config.",
origin, environment, requested_method);
assert_eq!(response.status(), StatusCode::OK, "{}", context);
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).map(|v| v.to_str().unwrap()), Some(origin), "{}", context);
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_CREDENTIALS).map(|v| v.to_str().unwrap()), Some("true"), "{}", context);
}
}
}
+5
View File
@@ -30,3 +30,8 @@ pub (crate) mod testing;
#[cfg(test)]
mod allowlist_regression_tests;
// Origins sent by shipped ArtCraft desktop builds. Kept apart from the configs
// they test so those can't be refactored away with them.
#[cfg(test)]
mod deployed_desktop_client_origin_tests;
+3
View File
@@ -12,6 +12,9 @@ pub const STORYTELLER_ENGINE: Domain = domain!("engine.storyteller.ai");
pub const STORYTELLER_STAGING: Domain = domain!("staging.storyteller.ai");
/// Storyteller Studio and its deploy environments.
///
/// NB: Shipped ArtCraft desktop builds send `https://studio.storyteller.ai` as
/// their CORS origin, so it must stay allowed (see `actix_cors_configs`).
pub const STORYTELLER_STUDIO: Domain = domain!("studio.storyteller.ai");
pub const STORYTELLER_STUDIO_STAGING: Domain = domain!("studio-staging.studio.storyteller.ai");
pub const STORYTELLER_STUDIO_TESTING: Domain = domain!("studio-testing.studio.storyteller.ai");