Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ add_library(flapi-lib STATIC
src/config_serializer.cpp
src/config_manager.cpp
src/config_service.cpp
src/cors_middleware.cpp
src/cors_policy.cpp
src/database_manager.cpp
src/endpoint_config_parser.cpp
src/extended_yaml_parser.cpp
Expand Down
46 changes: 43 additions & 3 deletions src/api_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,50 @@ void APIServer::setupRoutes() {
}

void APIServer::setupCORS() {
const auto& cors_cfg = configManager->getCorsConfig();

auto& cors = app.get_middleware<crow::CORSHandler>();
cors.global()
.headers("*")
.methods("GET"_method, "POST"_method, "PUT"_method, "PATCH"_method, "DELETE"_method);
auto& rules = cors.global();

// Crow's built-in CORSHandler covers methods + headers. Origin handling
// is intentionally left as the wildcard default here so it doesn't
// conflict with FlapiCorsMiddleware, which sets ACAO per request based
// on the configured allowlist (see cors_middleware.cpp).
if (cors_cfg.allow_headers.empty()) {
rules.headers("*");
} else {
for (const auto& h : cors_cfg.allow_headers) {
rules.headers(h);
}
}

if (cors_cfg.allow_methods.empty()) {
rules.methods("GET"_method, "POST"_method, "PUT"_method,
"PATCH"_method, "DELETE"_method);
} else {
for (const auto& m : cors_cfg.allow_methods) {
if (m == "GET") {
rules.methods("GET"_method);
} else if (m == "POST") {
rules.methods("POST"_method);
} else if (m == "PUT") {
rules.methods("PUT"_method);
} else if (m == "PATCH") {
rules.methods("PATCH"_method);
} else if (m == "DELETE") {
rules.methods("DELETE"_method);
} else if (m == "OPTIONS") {
rules.methods("OPTIONS"_method);
} else if (m == "HEAD") {
rules.methods("HEAD"_method);
}
}
}

// Hand the allowlist to the flapi-owned middleware so it can resolve
// the per-request `Access-Control-Allow-Origin` value.
auto& flapi_cors = app.get_middleware<FlapiCorsMiddleware>();
flapi_cors.initialize(configManager);
}

void APIServer::setupHeartbeat() {
Expand Down
28 changes: 28 additions & 0 deletions src/config_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,34 @@ OIDCConfig ConfigManager::parseOIDCConfigNode(const YAML::Node& oidc_node, const
}

// HTTPS configuration methods
void ConfigManager::parseCorsConfig() {
CROW_LOG_INFO << "Parsing CORS configuration";
cors_config = CorsConfig{}; // defaults to empty allowlist → wildcard

if (!config["cors"]) {
CROW_LOG_DEBUG << "CORS configuration not found, using defaults (Access-Control-Allow-Origin: *)";
return;
}

auto cors_node = config["cors"];
if (cors_node["allow-origins"]) {
for (const auto& entry : cors_node["allow-origins"]) {
cors_config.allow_origins.push_back(entry.as<std::string>());
}
}
if (cors_node["allow-headers"]) {
for (const auto& entry : cors_node["allow-headers"]) {
cors_config.allow_headers.push_back(entry.as<std::string>());
}
}
if (cors_node["allow-methods"]) {
for (const auto& entry : cors_node["allow-methods"]) {
cors_config.allow_methods.push_back(entry.as<std::string>());
}
}
CROW_LOG_DEBUG << "CORS allow-origins count: " << cors_config.allow_origins.size();
}

void ConfigManager::parseHttpsConfig() {
if (config["enforce-https"]) {
auto https_node = config["enforce-https"];
Expand Down
40 changes: 40 additions & 0 deletions src/cors_middleware.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "cors_middleware.hpp"

#include "config_manager.hpp"

namespace flapi {

void FlapiCorsMiddleware::initialize(std::shared_ptr<ConfigManager> config_manager) {
if (!config_manager) {
allow_origins_.clear();
return;
}
allow_origins_ = config_manager->getCorsConfig().allow_origins;
}

void FlapiCorsMiddleware::before_handle(crow::request& /*req*/, crow::response& /*res*/, context& /*ctx*/) {
// No-op. The policy applies on the response.
}

void FlapiCorsMiddleware::after_handle(crow::request& req, crow::response& res, context& /*ctx*/) {
std::string request_origin;
auto it = req.headers.find("Origin");
if (it != req.headers.end()) {
request_origin = it->second;
}

const auto resolved = policy_.resolveAllowedOrigin(request_origin, allow_origins_);
if (!resolved.has_value()) {
return; // No CORS header — browser blocks cross-origin access.
}

// Only set if Crow's CORSHandler hasn't already (it shouldn't have, by
// construction of the middleware order; defensive set_header avoids
// doubled headers regardless).
auto existing = res.headers.find("Access-Control-Allow-Origin");
if (existing == res.headers.end()) {
res.add_header("Access-Control-Allow-Origin", *resolved);
}
}

} // namespace flapi
53 changes: 53 additions & 0 deletions src/cors_policy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include "cors_policy.hpp"

#include <algorithm>

namespace flapi {

namespace {

bool containsWildcard(const std::vector<std::string>& allow_origins) {
return std::any_of(allow_origins.begin(), allow_origins.end(),
[](const std::string& v) { return v == CorsPolicy::kWildcard; });
}

bool containsExact(const std::vector<std::string>& allow_origins,
const std::string& origin) {
return std::find(allow_origins.begin(), allow_origins.end(), origin) != allow_origins.end();
}

} // namespace

std::optional<std::string> CorsPolicy::resolveAllowedOrigin(
const std::string& request_origin,
const std::vector<std::string>& allow_origins) const {

// Empty allowlist: keep the historic "*" default. This matters for the
// "simple stays simple" promise — fresh `flapii project init` projects
// must still work from a browser without any CORS configuration.
if (allow_origins.empty()) {
return std::string(kWildcard);
}

// Explicit wildcard always wins, even when combined with concrete
// entries. Mixing them is a configuration smell but the result is
// unambiguous.
if (containsWildcard(allow_origins)) {
return std::string(kWildcard);
}

// Same-origin / curl-style requests don't carry an Origin header.
// Returning nullopt is correct — no CORS response header is needed
// for same-origin requests; the browser doesn't enforce CORS on them.
if (request_origin.empty()) {
return std::nullopt;
}

if (containsExact(allow_origins, request_origin)) {
return request_origin;
}

return std::nullopt;
}

} // namespace flapi
8 changes: 7 additions & 1 deletion src/include/api_server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "auth_middleware.hpp"
#include "config_manager.hpp"
#include "cors_middleware.hpp"
#include "database_manager.hpp"
#include "heartbeat_worker.hpp"
#include "open_api_doc_generator.hpp"
Expand All @@ -19,7 +20,12 @@

namespace flapi {

using FlapiApp = crow::App<crow::CORSHandler, RateLimitMiddleware, AuthMiddleware>;
// Middleware order matters: `after_handle` runs in reverse order, so
// `FlapiCorsMiddleware` (sitting between `crow::CORSHandler` and the
// rest) gets its turn to set `Access-Control-Allow-Origin` BEFORE
// Crow's CORSHandler does. Crow uses `set_header_no_override`, so the
// origin we choose dynamically wins.
using FlapiApp = crow::App<crow::CORSHandler, FlapiCorsMiddleware, RateLimitMiddleware, AuthMiddleware>;

class ConfigService; // forward declaration
class HeartbeatWorker; // forward declaration
Expand Down
11 changes: 11 additions & 0 deletions src/include/config_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,14 @@ struct HttpsConfig {
std::string ssl_key_file;
};

// W1.2: CORS allowlist configuration. Empty fields preserve the historic
// wildcard behaviour so demo / init projects keep working in browsers.
struct CorsConfig {
std::vector<std::string> allow_origins; // empty → "*"; "*" → "*"; else allowlist
std::vector<std::string> allow_headers; // empty → "*"
std::vector<std::string> allow_methods; // empty → default GET/POST/PUT/PATCH/DELETE
};

struct GlobalHeartbeatConfig {
bool enabled = false;
std::chrono::seconds workerInterval = std::chrono::seconds(60);
Expand Down Expand Up @@ -495,6 +503,7 @@ class ConfigManager {
const RateLimitConfig& getRateLimitConfig() const;
const DuckDBConfig& getDuckDBConfig() const;
const HttpsConfig& getHttpsConfig() const;
const CorsConfig& getCorsConfig() const { return cors_config; }
bool isHttpsEnforced() const;
bool isAuthEnabled() const;
std::optional<OIDCConfig> getGlobalOIDCConfig() const;
Expand Down Expand Up @@ -571,6 +580,7 @@ class ConfigManager {
DuckDBConfig duckdb_config;
TemplateConfig template_config;
HttpsConfig https_config;
CorsConfig cors_config;
GlobalHeartbeatConfig global_heartbeat_config;
DuckLakeConfig ducklake_config;
MCPConfig mcp_config;
Expand All @@ -594,6 +604,7 @@ class ConfigManager {
void parseAuthConfig();
void parseDuckDBConfig();
void parseHttpsConfig();
void parseCorsConfig();
void parseTemplateConfig();
void parseDuckLakeConfig();
void parseMCPConfig();
Expand Down
41 changes: 41 additions & 0 deletions src/include/cors_middleware.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once

#include <crow/http_request.h>
#include <crow/http_response.h>
#include <memory>
#include <string>
#include <vector>

#include "cors_policy.hpp"

namespace flapi {

class ConfigManager;

// W1.2: Crow middleware that enforces the `cors.allow-origins` allowlist.
// Runs alongside `crow::CORSHandler` — it provides only the per-request
// `Access-Control-Allow-Origin` header, while Crow's CORSHandler keeps
// handling methods, headers, and max-age.
//
// The middleware reads the request's `Origin` header, asks `CorsPolicy`
// for the value it should advertise, and writes it directly onto the
// response. Crow's CORSHandler then sees the header is already set and
// declines to override it (see `set_header_no_override` in Crow).
class FlapiCorsMiddleware {
public:
struct context {};

// Must be initialised before the server accepts traffic. Safe to call
// multiple times if the operator hot-reloads the config — the new
// allow-origins list takes effect for the next request.
void initialize(std::shared_ptr<ConfigManager> config_manager);

void before_handle(crow::request& req, crow::response& res, context& ctx);
void after_handle(crow::request& req, crow::response& res, context& ctx);

private:
std::vector<std::string> allow_origins_;
CorsPolicy policy_;
};

} // namespace flapi
25 changes: 25 additions & 0 deletions src/include/cors_policy.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include <optional>
#include <string>
#include <vector>

namespace flapi {

// W1.2: CORS allowlist policy. Pure function over a configured allowlist
// and the request's Origin header. Returns the value that should land in
// the `Access-Control-Allow-Origin` response header, or std::nullopt
// when the request must not be granted any CORS access (browser blocks).
//
// Backward-compatibility rule: an empty allowlist preserves the
// historic "*" default so `flapii project init` demos keep working.
class CorsPolicy {
public:
static constexpr const char* kWildcard = "*";

std::optional<std::string> resolveAllowedOrigin(
const std::string& request_origin,
const std::vector<std::string>& allow_origins) const;
};

} // namespace flapi
3 changes: 2 additions & 1 deletion src/include/mcp_route_handlers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "crow/compression.h"

#include "config_manager.hpp"
#include "cors_middleware.hpp"
#include "database_manager.hpp"
#include "mcp_tool_handler.hpp"
#include "mcp_types.hpp"
Expand Down Expand Up @@ -43,7 +44,7 @@ class MCPRouteHandlers {
* @param app The Crow application to register routes with
* @param port The port number for the MCP server
*/
void registerRoutes(crow::App<crow::CORSHandler, RateLimitMiddleware, AuthMiddleware>& app, int port = 8080);
void registerRoutes(crow::App<crow::CORSHandler, FlapiCorsMiddleware, RateLimitMiddleware, AuthMiddleware>& app, int port = 8080);

/**
* Refresh MCP entities from the configuration.
Expand Down
5 changes: 3 additions & 2 deletions src/include/open_api_doc_generator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <memory>

#include "config_manager.hpp"
#include "cors_middleware.hpp"
#include "database_manager.hpp"
#include "auth_middleware.hpp"
#include "rate_limit_middleware.hpp"
Expand All @@ -16,8 +17,8 @@ namespace flapi {
class OpenAPIDocGenerator {
public:
OpenAPIDocGenerator(std::shared_ptr<ConfigManager> cm, std::shared_ptr<DatabaseManager> dm);
YAML::Node generateDoc(crow::App<crow::CORSHandler, RateLimitMiddleware, AuthMiddleware>& app);
YAML::Node generateConfigServiceDoc(crow::App<crow::CORSHandler, RateLimitMiddleware, AuthMiddleware>& app);
YAML::Node generateDoc(crow::App<crow::CORSHandler, FlapiCorsMiddleware, RateLimitMiddleware, AuthMiddleware>& app);
YAML::Node generateConfigServiceDoc(crow::App<crow::CORSHandler, FlapiCorsMiddleware, RateLimitMiddleware, AuthMiddleware>& app);

private:
std::shared_ptr<ConfigManager> configManager;
Expand Down
2 changes: 1 addition & 1 deletion src/mcp_route_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ MCPRouteHandlers::MCPRouteHandlers(std::shared_ptr<ConfigManager> config_manager
CROW_LOG_INFO << "Transport type: Streamable HTTP, URL ready to paste into MCP inspector tool";
}

void MCPRouteHandlers::registerRoutes(crow::App<crow::CORSHandler, RateLimitMiddleware, AuthMiddleware>& app, int port) {
void MCPRouteHandlers::registerRoutes(crow::App<crow::CORSHandler, FlapiCorsMiddleware, RateLimitMiddleware, AuthMiddleware>& app, int port) {
port_ = port; // Update port if provided

CROW_LOG_INFO << "Registering MCP routes with application...";
Expand Down
4 changes: 2 additions & 2 deletions src/open_api_doc_generator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace flapi {
OpenAPIDocGenerator::OpenAPIDocGenerator(std::shared_ptr<ConfigManager> cm, std::shared_ptr<DatabaseManager> dm)
: configManager(cm), dbManager(dm) {}

YAML::Node OpenAPIDocGenerator::generateDoc(crow::App<crow::CORSHandler, RateLimitMiddleware, AuthMiddleware>& app)
YAML::Node OpenAPIDocGenerator::generateDoc(crow::App<crow::CORSHandler, FlapiCorsMiddleware, RateLimitMiddleware, AuthMiddleware>& app)
{
YAML::Node doc;

Expand Down Expand Up @@ -275,7 +275,7 @@ YAML::Node OpenAPIDocGenerator::generateResponseSchema(const EndpointConfig& end
return schema;
}

YAML::Node OpenAPIDocGenerator::generateConfigServiceDoc(crow::App<crow::CORSHandler, RateLimitMiddleware, AuthMiddleware>& app) {
YAML::Node OpenAPIDocGenerator::generateConfigServiceDoc(crow::App<crow::CORSHandler, FlapiCorsMiddleware, RateLimitMiddleware, AuthMiddleware>& app) {
YAML::Node doc;

// OpenAPI version
Expand Down
1 change: 1 addition & 0 deletions test/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ add_executable(flapi_tests
config_manager_path_resolution_test.cpp
config_service_test.cpp
config_service_filesystem_test.cpp
cors_policy_test.cpp
config_service_parameters_test.cpp
config_service_slug_test.cpp
config_service_template_lookup_test.cpp
Expand Down
Loading
Loading