1use serde::{Deserialize, Serialize};
2use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme};
3use utoipa::{Modify, OpenApi, ToSchema};
4
5pub use crate::control_resources::*;
6use crate::health::HealthResponse;
7
8#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
10pub struct ServiceDescriptor {
11 pub schema_version: String,
13 pub product: String,
15 pub instance_id: String,
17 pub api_base_url: String,
19 pub api_versions: Vec<String>,
21 pub credential_class: String,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub mcp_url: Option<String>,
26}
27
28#[utoipa::path(
29 get,
30 path = "/.well-known/owlauth",
31 responses(
32 (status = 200, description = "Public OwlAuth endpoint descriptor", body = ServiceDescriptor)
33 )
34)]
35#[doc(hidden)]
36#[must_use]
37pub fn get_service_descriptor() -> ServiceDescriptor {
38 ServiceDescriptor {
39 schema_version: "1".to_owned(),
40 product: "owlauth-server".to_owned(),
41 instance_id: "deployment-public-id".to_owned(),
42 api_base_url: "https://admin.example.com/v1/".to_owned(),
43 api_versions: vec!["v1".to_owned()],
44 credential_class: "operator-api-key".to_owned(),
45 mcp_url: None,
46 }
47}
48
49#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
51pub struct SystemCapabilities {
52 pub product: String,
54 pub provisioning: bool,
56 pub login_readiness: bool,
58 pub federated_project_auth: bool,
60}
61
62#[utoipa::path(
63 get,
64 path = "/v1/system",
65 responses(
66 (status = 200, description = "Authenticated deployment capabilities", body = SystemCapabilities),
67 (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge")))
68 ),
69 security(("operator_api_key" = []))
70)]
71#[doc(hidden)]
72#[must_use]
73pub fn get_system() -> SystemCapabilities {
74 SystemCapabilities {
75 product: "owlauth-server".to_owned(),
76 provisioning: true,
77 login_readiness: true,
78 federated_project_auth: crate::FEDERATED_PROJECT_AUTH_AVAILABLE,
79 }
80}
81
82#[derive(OpenApi)]
83#[openapi(
84 info(
85 title = "OwlAuth Control API",
86 description = "Deployment Control API"
87 ),
88 paths(
89 crate::health::get_liveness,
90 crate::health::get_readiness,
91 get_service_descriptor,
92 get_system,
93 crate::control_resources::list_projects,
94 crate::control_resources::create_project,
95 crate::control_resources::get_project,
96 crate::control_resources::update_project,
97 crate::control_resources::get_project_overview,
98 crate::control_resources::get_project_policy,
99 crate::control_resources::update_project_policy,
100 crate::control_resources::disable_project,
101 crate::control_resources::list_project_server_keys,
102 crate::control_resources::get_project_server_key,
103 crate::control_resources::create_project_server_key,
104 crate::control_resources::acknowledge_project_server_key_delivery,
105 crate::control_resources::revoke_project_server_key,
106 crate::control_resources::list_applications,
107 crate::control_resources::create_application,
108 crate::control_resources::get_application,
109 crate::control_resources::update_application,
110 crate::control_resources::replace_application_configuration,
111 crate::control_resources::disable_application,
112 crate::control_resources::list_webhook_endpoints,
113 crate::control_resources::create_webhook_endpoint,
114 crate::control_resources::get_webhook_endpoint,
115 crate::control_resources::update_webhook_endpoint,
116 crate::control_resources::test_webhook_endpoint,
117 crate::control_resources::activate_webhook_endpoint,
118 crate::control_resources::disable_webhook_endpoint,
119 crate::control_resources::prepare_webhook_secret_rotation,
120 crate::control_resources::activate_webhook_secret_rotation,
121 crate::control_resources::list_application_user_events,
122 crate::control_resources::list_webhook_deliveries,
123 crate::control_resources::get_webhook_delivery,
124 crate::control_resources::replay_webhook_delivery,
125 crate::control_resources::list_signing_keys,
126 crate::control_resources::rotate_signing_key,
127 crate::control_resources::revoke_signing_key,
128 crate::control_resources::get_provider_egress_policy,
129 crate::control_resources::update_provider_egress_policy,
130 crate::control_resources::preflight_oidc_provider,
131 crate::control_resources::preflight_named_provider,
132 crate::control_resources::list_providers,
133 crate::control_resources::create_provider,
134 crate::control_resources::update_provider,
135 crate::control_resources::replace_provider_secret,
136 crate::control_resources::reconcile_provider_secret_replacement,
137 crate::control_resources::abandon_provider_secret_replacement,
138 crate::control_resources::reconcile_provider,
139 crate::control_resources::disable_provider,
140 crate::control_resources::assign_provider,
141 crate::control_resources::unassign_provider,
142 crate::control_resources::get_email_method_policy,
143 crate::control_resources::update_email_method_policy,
144 crate::control_resources::list_email_assignments,
145 crate::control_resources::assign_email_method,
146 crate::control_resources::list_deployment_smtp_generations,
147 crate::control_resources::reconcile_deployment_smtp_generation,
148 crate::control_resources::disable_deployment_smtp_generation,
149 crate::control_resources::compromise_deployment_smtp_generation,
150 crate::control_resources::list_smtp_configurations,
151 crate::control_resources::create_smtp_configuration,
152 crate::control_resources::test_smtp_configuration,
153 crate::control_resources::get_smtp_test_operation,
154 crate::control_resources::activate_smtp_configuration,
155 crate::control_resources::disable_smtp_configuration,
156 crate::control_resources::compromise_smtp_configuration,
157 crate::control_resources::list_project_users,
158 crate::control_resources::lookup_project_user_by_email,
159 crate::control_resources::get_project_user,
160 crate::control_resources::list_project_user_identities,
161 crate::control_resources::disable_project_user,
162 crate::control_resources::enable_project_user,
163 crate::control_resources::list_project_user_sessions,
164 crate::control_resources::revoke_application_session,
165 crate::control_resources::revoke_browser_session,
166 crate::control_resources::list_managed_provider_connections,
167 crate::control_resources::synchronize_managed_provider_connection,
168 crate::control_resources::create_managed_reauthorization,
169 crate::control_resources::get_managed_reauthorization,
170 crate::control_resources::cancel_managed_reauthorization,
171 crate::control_resources::revoke_managed_provider_connection,
172 crate::control_resources::disconnect_managed_provider_connection,
173 crate::control_resources::create_identity_mutation_intent,
174 crate::control_resources::get_identity_mutation_intent,
175 crate::control_resources::cancel_identity_mutation_intent,
176 crate::control_resources::confirm_identity_mutation_intent
177 ),
178 components(schemas(
179 HealthResponse,
180 ServiceDescriptor,
181 SystemCapabilities,
182 ProblemDetails,
183 ProjectStatus,
184 ApplicationType,
185 ApplicationStatus,
186 SigningKeyState,
187 ProviderStatus,
188 crate::runtime::ProviderKind,
189 crate::runtime::JwkKeyType,
190 crate::runtime::JwkCurve,
191 crate::runtime::SigningAlgorithm,
192 crate::runtime::JwkUse,
193 crate::runtime::PublicJwk,
194 Project,
195 ProjectList,
196 ProjectOverviewSummary,
197 ProjectOverviewApplicationCounts,
198 ProjectOverviewProviderCounts,
199 ProjectOverviewUserCounts,
200 ProjectOverviewServerKeyCounts,
201 CreateProjectRequest,
202 UpdateProjectRequest,
203 ProjectPolicy,
204 UpdateProjectPolicyRequest,
205 ExpectedSecurityRevision,
206 ApplicationConfiguration,
207 Application,
208 ApplicationList,
209 CreateApplicationRequest,
210 UpdateApplicationRequest,
211 ReplaceApplicationConfigurationRequest,
212 WebhookEndpointStatus,
213 ApplicationUserEventType,
214 WebhookDeliveryState,
215 WebhookDeliveryOutcomeClass,
216 WebhookEndpoint,
217 WebhookEndpointList,
218 CreateWebhookEndpointRequest,
219 UpdateWebhookEndpointRequest,
220 ExpectedWebhookEndpointRevision,
221 PrepareWebhookSecretRotationRequest,
222 WebhookSecretPreparationStatus,
223 PreparedWebhookSecretRotation,
224 ActivateWebhookSecretRotationRequest,
225 ApplicationUserEvent,
226 ApplicationUserEventList,
227 WebhookDelivery,
228 WebhookDeliveryList,
229 ReplayWebhookDeliveryRequest,
230 SigningKey,
231 SigningKeyList,
232 ProjectServerKeyStatus,
233 ProjectServerKey,
234 ProjectServerKeyList,
235 CreateProjectServerKeyRequest,
236 CreateProjectServerKeyResponse,
237 AcknowledgeProjectServerKeyDeliveryRequest,
238 RevokeProjectServerKeyRequest,
239 RotateSigningKeyRequest,
240 KeyTransitionRequest,
241 Provider,
242 ProviderManagedProfileCapability,
243 ProviderList,
244 ProviderEgressMode,
245 ProviderEgressPolicy,
246 UpdateProviderEgressPolicyRequest,
247 OidcPreflightRequest,
248 OidcPreflightResult,
249 NamedProviderPreflightRequest,
250 NamedProviderPreflightResult,
251 ProviderCallbackGuidance,
252 ProviderConsentBehavior,
253 FixedProviderAuthorizationPolicy,
254 CreateProviderRequest,
255 UpdateProviderRequest,
256 ReplaceProviderSecretRequest,
257 ReconcileProviderRequest,
258 ReconcileProviderSecretReplacementRequest,
259 ProviderRevisionRequest,
260 ProviderAssignmentRequest,
261 SmtpTlsMode,
262 SmtpGenerationStatus,
263 EmailMethodPolicy,
264 UpdateEmailMethodPolicyRequest,
265 EmailAssignmentRequest,
266 EmailAssignment,
267 EmailAssignmentList,
268 SmtpConfiguration,
269 SmtpConfigurationList,
270 DeploymentSmtpGeneration,
271 DeploymentSmtpGenerationList,
272 ReconcileDeploymentSmtpRequest,
273 CreateSmtpConfigurationRequest,
274 SmtpRevisionRequest,
275 TestSmtpConfigurationRequest,
276 ManagedProviderConnectionState,
277 ManagedProviderConnection,
278 ManagedProviderConnectionList,
279 ManagedProviderConnectionActionRequest,
280 ManagedReauthorizationStatus,
281 ManagedReauthorization,
282 CreateManagedReauthorizationRequest,
283 CreateManagedReauthorizationResponse,
284 CancelManagedReauthorizationRequest,
285 crate::runtime::IdentityKind,
286 crate::runtime::IdentityMutationMethodKind,
287 IdentityMutationUserTarget,
288 ExistingIdentityReference,
289 IdentityMutationProofAuthority,
290 UnlinkPrimarySourceDisposition,
291 MergePrimarySource,
292 MergeSessionsDisposition,
293 MergeBindingsDisposition,
294 CreateIdentityMutationIntentRequest,
295 IdentityMutationOperationKind,
296 IdentityMutationIntentStatus,
297 IdentityMutationProofRole,
298 IdentityMutationProofSlot,
299 IdentityMutationIntent,
300 CreateIdentityMutationIntentResponse,
301 CancelIdentityMutationIntentRequest,
302 LinkIdentityMutationConfirmation,
303 UnlinkIdentityMutationConfirmation,
304 MergeIdentityMutationConfirmation,
305 ConfirmIdentityMutationIntentRequest,
306 ProjectUserStatus,
307 ProjectUserSort,
308 ProjectUserIdentityFilter,
309 ManagedSessionStatus,
310 ProjectUser,
311 ProjectUserList,
312 ProjectUserEmailLookupRequest,
313 ProjectUserLookup,
314 ProjectUserIdentityStatus,
315 RedactedEmailMarker,
316 ProjectUserIdentityPresentation,
317 ProjectUserIdentity,
318 ProjectUserIdentityList,
319 ApplicationSession,
320 BrowserSession,
321 ProjectUserSessions,
322 ExpectedSessionRevision
323 )),
324 modifiers(&ControlSecurity)
325)]
326struct ControlApiDoc;
327
328struct ControlSecurity;
329
330impl Modify for ControlSecurity {
331 fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
332 openapi
333 .components
334 .get_or_insert_default()
335 .add_security_scheme(
336 "operator_api_key",
337 SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
338 );
339 }
340}
341
342#[must_use]
344pub fn openapi() -> utoipa::openapi::OpenApi {
345 let mut document = ControlApiDoc::openapi();
346 crate::add_response_to_operations(&mut document, "408", |_| {
347 crate::json_error_response(
348 "The request exceeded the Control listener time budget",
349 "ProblemDetails",
350 "application/problem+json",
351 )
352 });
353 document
354}
355
356#[cfg(test)]
357mod identity_inventory_tests {
358 use super::*;
359
360 #[test]
361 fn application_sync_contract_is_typed_bounded_and_control_only() {
362 let document = serde_json::to_value(openapi()).expect("serialize Control OpenAPI");
363 for path in [
364 "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints",
365 "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}",
366 "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}/test",
367 "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}/activate",
368 "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}/secret-rotations",
369 "/v1/projects/{project_id}/applications/{application_id}/user-events",
370 "/v1/projects/{project_id}/applications/{application_id}/webhook-deliveries",
371 "/v1/projects/{project_id}/applications/{application_id}/webhook-deliveries/{delivery_id}/replay",
372 ] {
373 assert!(document["paths"][path].is_object(), "missing path: {path}");
374 }
375 assert_eq!(
376 document["components"]["schemas"]["CreateWebhookEndpointRequest"]["properties"]["secret"]
377 ["writeOnly"],
378 true
379 );
380 assert_eq!(
381 document["components"]["schemas"]["WebhookEndpointList"]["properties"]["items"]["maxItems"],
382 100
383 );
384 let endpoint = document["components"]["schemas"]["WebhookEndpoint"].to_string();
385 assert!(!endpoint.contains("secret_ref"));
386 assert!(!endpoint.contains("request_fingerprint"));
387 let runtime =
388 serde_json::to_value(crate::runtime::openapi()).expect("serialize Runtime OpenAPI");
389 assert!(
390 runtime["paths"]
391 ["/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints"]
392 .is_null()
393 );
394 }
395
396 #[test]
397 fn email_assignment_read_model_is_bounded_and_control_only() {
398 let document = serde_json::to_value(openapi()).expect("serialize Control OpenAPI");
399 let path = "/v1/projects/{project_id}/email-method/assignments";
400 assert!(document["paths"][path]["get"].is_object());
401 assert_eq!(
402 document["components"]["schemas"]["EmailAssignmentList"]["properties"]["items"]["maxItems"],
403 100
404 );
405 assert_eq!(
406 document["components"]["schemas"]["EmailAssignment"]["properties"]["security_revision"]
407 ["minimum"],
408 1
409 );
410 let runtime =
411 serde_json::to_value(crate::runtime::openapi()).expect("serialize Runtime OpenAPI");
412 assert!(runtime["paths"].get(path).is_none());
413 }
414
415 #[test]
416 fn identity_inventory_contract_is_bounded_redacted_and_control_only() {
417 let document = serde_json::to_value(openapi()).expect("serialize Control OpenAPI");
418 assert!(
419 document["paths"]
420 .get("/v1/projects/{project_id}/users/{user_id}/identities")
421 .is_some()
422 );
423 assert_eq!(
424 document["components"]["schemas"]["ProjectUserIdentityList"]["properties"]["items"]["maxItems"],
425 100
426 );
427 assert_eq!(
428 document["components"]["schemas"]["ProjectUserLookup"]["required"],
429 serde_json::json!(["user"])
430 );
431 let identity = document["components"]["schemas"]["ProjectUserIdentity"].to_string();
432 for forbidden in [
433 "issuer",
434 "subject",
435 "ciphertext",
436 "digest",
437 "alias",
438 "client_id",
439 "secret",
440 "credential",
441 "receipt",
442 "evidence",
443 "raw_email",
444 ] {
445 assert!(
446 !identity.contains(forbidden),
447 "safe inventory schema exposed forbidden field {forbidden}"
448 );
449 }
450 let runtime =
451 serde_json::to_value(crate::runtime::openapi()).expect("serialize Runtime OpenAPI");
452 assert!(
453 runtime["paths"]
454 .get("/v1/projects/{project_id}/users/{user_id}/identities")
455 .is_none()
456 );
457 }
458}