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