postrust_proxy/saas/handlers/
mod.rs1pub mod auth;
4pub mod domains;
5pub mod routes;
6pub mod upstreams;
7pub mod wellknown;
8
9use crate::admin::api::ApiResponse;
10use crate::error::{ProxyError, ProxyResult};
11use crate::saas::api_keys::ApiKeyService;
12use crate::saas::auth::{AuthContext, SaasAuthLayer};
13use crate::saas::manager::DomainManager;
14use crate::saas::options::SaasOptions;
15use crate::saas::verification::DomainVerificationService;
16use crate::tls::CertificateStore;
17use axum::{
18 http::StatusCode,
19 middleware,
20 routing::{delete, get, post, put},
21 Router,
22};
23use sqlx::PgPool;
24use std::sync::Arc;
25
26#[derive(Clone)]
28pub struct SaasState {
29 pub domain_manager: Arc<DomainManager>,
31 pub api_key_service: Arc<ApiKeyService>,
33 pub auth_layer: Arc<SaasAuthLayer>,
37 pub pool: PgPool,
44
45 pub cert_store: Arc<CertificateStore>,
47}
48
49impl SaasState {
50 pub async fn new(
61 pool: PgPool,
62 jwt_secret: Option<String>,
63 cert_dir: impl AsRef<std::path::Path>,
64 ) -> ProxyResult<Self> {
65 let options = SaasOptions {
66 jwt_secret,
67 ..SaasOptions::default()
68 };
69 Self::with_options(pool, options, cert_dir).await
70 }
71
72 pub async fn with_options(
74 pool: PgPool,
75 options: SaasOptions,
76 cert_dir: impl AsRef<std::path::Path>,
77 ) -> ProxyResult<Self> {
78 let verification_service = Arc::new(DomainVerificationService::new());
79 let domain_manager = Arc::new(DomainManager::with_policy(
80 pool.clone(),
81 verification_service,
82 options.domains,
83 ));
84 let api_key_service = Arc::new(ApiKeyService::new(pool.clone()));
85 let pool_for_handlers = pool.clone();
86 let auth_layer = Arc::new(
87 SaasAuthLayer::new(pool, options.jwt_secret)
88 .with_jwt_audience(options.jwt_audience)
89 .with_jwt_issuer(options.jwt_issuer),
90 );
91
92 let cert_store =
93 Arc::new(CertificateStore::new(pool_for_handlers.clone(), cert_dir).await?);
94
95 Ok(Self {
96 domain_manager,
97 api_key_service,
98 auth_layer,
99 pool: pool_for_handlers,
100 cert_store,
101 })
102 }
103}
104
105pub fn saas_router(state: SaasState) -> Router {
107 let auth_layer = state.auth_layer.clone();
108
109 let public_routes = Router::new().route(
111 "/.well-known/postrust-verification/{token}",
112 get(wellknown::handle_verification_challenge),
113 );
114
115 #[cfg(feature = "acme")]
119 let public_routes = public_routes.route(
120 "/.well-known/acme-challenge/{token}",
121 get(wellknown::handle_acme_challenge),
122 );
123
124 let protected_routes = Router::new();
126
127 #[cfg(feature = "acme")]
132 let protected_routes =
133 protected_routes.route("/domains/{id}/ssl/provision", post(domains::provision_ssl));
134
135 let protected_routes = protected_routes
136 .route("/auth/api-keys", post(auth::create_api_key))
138 .route("/auth/api-keys", get(auth::list_api_keys))
139 .route("/auth/api-keys/{id}", delete(auth::revoke_api_key))
140 .route("/tenant/me", get(auth::get_current_tenant))
142 .route("/tenant/usage", get(auth::get_tenant_usage))
143 .route("/domains", get(domains::list_domains))
145 .route("/domains", post(domains::create_domain))
146 .route("/domains/{id}", get(domains::get_domain))
147 .route("/domains/{id}", delete(domains::delete_domain))
148 .route("/domains/{id}/verify", post(domains::verify_domain))
149 .route("/domains/{id}/enable", post(domains::enable_domain))
150 .route("/domains/{id}", put(domains::update_domain))
151 .route("/domains/{id}/disable", post(domains::disable_domain))
152 .route(
153 "/domains/{id}/ssl/upload",
154 post(domains::upload_certificate),
155 )
156 .route("/domains/{domain_id}/routes", get(routes::list_routes))
158 .route("/domains/{domain_id}/routes", post(routes::create_route))
159 .route("/domains/{domain_id}/routes/{id}", get(routes::get_route))
160 .route(
161 "/domains/{domain_id}/routes/{id}",
162 put(routes::update_route),
163 )
164 .route(
165 "/domains/{domain_id}/routes/{id}",
166 delete(routes::delete_route),
167 )
168 .route("/upstreams", get(upstreams::list_upstreams))
170 .route("/upstreams", post(upstreams::create_upstream))
171 .route("/upstreams/{id}", get(upstreams::get_upstream))
172 .route("/upstreams/{id}", put(upstreams::update_upstream))
173 .route("/upstreams/{id}", delete(upstreams::delete_upstream))
174 .route("/upstreams/{id}/backends", post(upstreams::add_backend))
175 .route(
176 "/upstreams/{id}/backends/{backend_id}",
177 delete(upstreams::remove_backend),
178 )
179 .layer(middleware::from_fn_with_state(
180 auth_layer,
181 crate::saas::auth::auth_middleware,
182 ))
183 .with_state(state.clone());
184
185 Router::new()
187 .merge(public_routes.with_state(state.clone()))
188 .nest("/api/v1", protected_routes)
189}
190
191pub fn error_response(error: ProxyError) -> (StatusCode, axum::Json<ApiResponse<()>>) {
203 let (status, code, message) = match error {
204 ProxyError::Validation(message) => (StatusCode::BAD_REQUEST, "validation_failed", message),
205 ProxyError::NotFound(message) => (StatusCode::NOT_FOUND, "not_found", message),
206 ProxyError::Conflict(message) => (StatusCode::CONFLICT, "conflict", message),
207 ProxyError::QuotaExceeded(message) => {
208 (StatusCode::PAYMENT_REQUIRED, "quota_exceeded", message)
209 }
210 ProxyError::Forbidden(message) => (StatusCode::FORBIDDEN, "forbidden", message),
211 ProxyError::Auth(message) => (StatusCode::UNAUTHORIZED, "unauthorized", message),
212 ProxyError::Verification(message) => {
213 (StatusCode::BAD_REQUEST, "verification_failed", message)
214 }
215 ProxyError::TenantSuspended => (
216 StatusCode::FORBIDDEN,
217 "tenant_suspended",
218 "Tenant account is suspended".to_string(),
219 ),
220 ProxyError::RateLimitExceeded => (
221 StatusCode::TOO_MANY_REQUESTS,
222 "rate_limited",
223 "Too many attempts; try again later".to_string(),
224 ),
225 ProxyError::Database(error) => return database_error(error),
226 other => {
227 tracing::error!(error = %other, "SaaS API request failed");
228 (
229 StatusCode::INTERNAL_SERVER_ERROR,
230 "internal_error",
231 "Internal server error".to_string(),
232 )
233 }
234 };
235 (
236 status,
237 axum::Json(ApiResponse::<()>::error_with_code(code, message)),
238 )
239}
240
241fn database_error(error: sqlx::Error) -> (StatusCode, axum::Json<ApiResponse<()>>) {
249 let kind = error.as_database_error().map(|e| e.kind());
250 let (status, code, message) = match kind {
251 Some(sqlx::error::ErrorKind::UniqueViolation) => (
252 StatusCode::CONFLICT,
253 "conflict",
254 "A resource with these values already exists",
255 ),
256 Some(sqlx::error::ErrorKind::ForeignKeyViolation) => (
257 StatusCode::CONFLICT,
258 "conflict",
259 "The request refers to a resource that does not exist or is still in use",
260 ),
261 Some(sqlx::error::ErrorKind::CheckViolation | sqlx::error::ErrorKind::NotNullViolation) => {
262 (
263 StatusCode::BAD_REQUEST,
264 "validation_failed",
265 "A value in the request is not allowed",
266 )
267 }
268 _ => {
269 tracing::error!(%error, "SaaS API database error");
270 (
271 StatusCode::INTERNAL_SERVER_ERROR,
272 "internal_error",
273 "Internal server error",
274 )
275 }
276 };
277 if status.is_client_error() {
278 tracing::debug!(%error, "SaaS API request refused by a constraint");
279 }
280 (
281 status,
282 axum::Json(ApiResponse::<()>::error_with_code(code, message)),
283 )
284}
285
286pub(crate) fn require_scope(
292 auth: &AuthContext,
293 scope: &str,
294) -> Result<(), axum::response::Response> {
295 use axum::response::IntoResponse;
296
297 if auth.grants(scope) {
298 return Ok(());
299 }
300 Err((
301 StatusCode::FORBIDDEN,
302 axum::Json(ApiResponse::<()>::error_with_code(
303 "insufficient_scope",
304 format!("Insufficient permissions: this requires the {scope} scope"),
305 )),
306 )
307 .into_response())
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 fn render(error: ProxyError) -> (StatusCode, serde_json::Value) {
315 let (status, axum::Json(body)) = error_response(error);
316 (status, serde_json::to_value(body).unwrap())
317 }
318
319 #[test]
320 fn errors_of_ours_are_not_described() {
321 for error in [
322 ProxyError::Database(sqlx::Error::PoolTimedOut),
323 ProxyError::Database(sqlx::Error::Protocol(
324 "relation \"proxy_domains\" does not exist".into(),
325 )),
326 ProxyError::Io(std::io::Error::other(
327 "/var/lib/postrust/certs: permission denied",
328 )),
329 ProxyError::Tls("bad key".into()),
330 ProxyError::Internal("secret detail".into()),
331 ] {
332 let (status, body) = render(error);
333 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
334 assert_eq!(body["code"], "internal_error");
335 assert_eq!(body["error"], "Internal server error");
336 }
337 }
338
339 #[test]
340 fn errors_about_the_request_keep_their_message() {
341 let (status, body) = render(ProxyError::Validation("the domain is empty".into()));
342 assert_eq!(status, StatusCode::BAD_REQUEST);
343 assert_eq!(body["code"], "validation_failed");
344 assert_eq!(body["error"], "the domain is empty");
345
346 let (status, body) = render(ProxyError::RateLimitExceeded);
347 assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
348 assert_eq!(body["code"], "rate_limited");
349 }
350}