Skip to content

Commit 2619e6b

Browse files
committed
feat: s2s webhook expansion
1 parent 5964c5f commit 2619e6b

File tree

17 files changed

+956
-1
lines changed

17 files changed

+956
-1
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ jsonwebtoken = { version = "10.1", default-features = false, features = [
7979
"ed25519-dalek",
8080
"use_pem",
8181
] }
82+
pem = "3.0"
8283
secrecy = "0.10"
8384
sha2 = "0.10"
8485
uuid = { version = "1.0", features = ["serde", "v4"] }
@@ -109,6 +110,9 @@ time = { version = "0.3", features = ["macros", "formatting"] }
109110
# Regular expressions
110111
regex = "1.11"
111112

113+
# URL parsing
114+
url = "2.5"
115+
112116
# Validation
113117
validator = { version = "0.20", features = ["derive"] }
114118

config.integration.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,13 @@ id_generation:
4949
server_api:
5050
grpc_endpoint: "http://server:8080" # Docker service name
5151
tls_enabled: false
52+
53+
management_identity:
54+
management_id: "management-integration-test"
55+
kid: "mgmt-test-2024"
56+
57+
cache_invalidation:
58+
http_endpoints:
59+
- "http://server:8080"
60+
timeout_ms: 5000
61+
retry_attempts: 0

crates/infera-management-api/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ axum = { workspace = true }
2121
axum-extra = { workspace = true }
2222
base64 = { workspace = true }
2323
chrono = { workspace = true }
24+
ed25519-dalek = { workspace = true }
2425
jsonwebtoken = { workspace = true }
2526
metrics-exporter-prometheus = { workspace = true }
2627
once_cell = "1.20"
@@ -35,5 +36,6 @@ tokio = { workspace = true }
3536
tower = { workspace = true }
3637
tower-http = { workspace = true }
3738
tracing = { workspace = true }
39+
uuid = { version = "1.11", features = ["v4", "serde"] }
3840

3941
[dev-dependencies]

crates/infera-management-api/src/handlers/auth.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ pub struct AppState {
3636
pub start_time: std::time::SystemTime,
3737
pub leader: Option<Arc<infera_management_core::LeaderElection<Backend>>>,
3838
pub email_service: Option<Arc<infera_management_core::EmailService>>,
39+
pub webhook_client: Option<Arc<infera_management_core::WebhookClient>>,
40+
pub management_identity: Option<Arc<infera_management_types::ManagementIdentity>>,
3941
}
4042

4143
impl AppState {
@@ -46,6 +48,8 @@ impl AppState {
4648
worker_id: u16,
4749
leader: Option<Arc<infera_management_core::LeaderElection<Backend>>>,
4850
email_service: Option<Arc<infera_management_core::EmailService>>,
51+
webhook_client: Option<Arc<infera_management_core::WebhookClient>>,
52+
management_identity: Option<Arc<infera_management_types::ManagementIdentity>>,
4953
) -> Self {
5054
Self {
5155
storage,
@@ -55,6 +59,8 @@ impl AppState {
5559
start_time: std::time::SystemTime::now(),
5660
leader,
5761
email_service,
62+
webhook_client,
63+
management_identity,
5864
}
5965
}
6066

@@ -128,6 +134,8 @@ server_api:
128134
start_time: std::time::SystemTime::now(),
129135
leader: None,
130136
email_service: Some(Arc::new(email_service)),
137+
webhook_client: None, // No webhook client in tests
138+
management_identity: None, // No management identity in tests
131139
}
132140
}
133141
}

crates/infera-management-api/src/handlers/jwks.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,49 @@ pub async fn get_org_jwks(
163163
Ok(Json(JwksResponse { keys }))
164164
}
165165

166+
/// Get Management API's public JWKS (for server-to-server authentication)
167+
///
168+
/// GET /.well-known/management-jwks.json
169+
/// Public endpoint - no authentication required
170+
///
171+
/// This endpoint returns the Management API's own public key, which servers use
172+
/// to validate JWTs signed by the Management API when receiving webhook callbacks.
173+
pub async fn get_management_jwks(
174+
State(state): State<AppState>,
175+
) -> Result<Json<JwksResponse>, (StatusCode, String)> {
176+
// Get the management identity from AppState
177+
let identity = state
178+
.management_identity
179+
.as_ref()
180+
.ok_or_else(|| {
181+
(
182+
StatusCode::SERVICE_UNAVAILABLE,
183+
"Management identity not configured".to_string(),
184+
)
185+
})?;
186+
187+
let jwks = identity.to_jwks();
188+
189+
// Convert from infera_management_types::identity::Jwks to our JwksResponse
190+
// The types are structurally identical, so we can convert the fields
191+
let keys = jwks
192+
.keys
193+
.into_iter()
194+
.map(|k| Jwk {
195+
kty: k.kty,
196+
crv: k.crv,
197+
kid: k.kid,
198+
x: k.x,
199+
key_use: k.key_use,
200+
alg: k.alg,
201+
})
202+
.collect();
203+
204+
let response = JwksResponse { keys };
205+
206+
Ok(Json(response))
207+
}
208+
166209
#[cfg(test)]
167210
mod tests {
168211
use super::*;

crates/infera-management-api/src/handlers/organizations.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,11 @@ pub async fn delete_organization(
374374
// Finally, soft delete the organization
375375
repos.org.delete(org_ctx.organization_id).await?;
376376

377+
// Invalidate caches on all servers
378+
if let Some(ref webhook_client) = state.webhook_client {
379+
webhook_client.invalidate_organization(org_ctx.organization_id).await;
380+
}
381+
377382
Ok(Json(DeleteOrganizationResponse {
378383
message: "Organization deleted successfully".to_string(),
379384
}))

crates/infera-management-api/src/handlers/vaults.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@ pub async fn update_vault(
289289
// Save changes
290290
repos.vault.update(vault.clone()).await?;
291291

292+
// Invalidate caches on all servers (vault metadata changed)
293+
if let Some(ref webhook_client) = state.webhook_client {
294+
webhook_client.invalidate_vault(vault_id).await;
295+
}
296+
292297
Ok(Json(UpdateVaultResponse {
293298
vault: VaultDetail {
294299
id: vault.id,
@@ -363,6 +368,11 @@ pub async fn delete_vault(
363368
vault.mark_deleted();
364369
repos.vault.update(vault).await?;
365370

371+
// Invalidate caches on all servers
372+
if let Some(ref webhook_client) = state.webhook_client {
373+
webhook_client.invalidate_vault(vault_id).await;
374+
}
375+
366376
Ok(StatusCode::NO_CONTENT)
367377
}
368378

crates/infera-management-api/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod pagination;
1313
pub mod routes;
1414

1515
pub use handlers::AppState;
16+
pub use infera_management_types::identity::{ManagementIdentity, SharedManagementIdentity};
1617
pub use infera_management_types::dto::ErrorResponse;
1718
pub use middleware::{
1819
extract_session_context, get_user_vault_role, require_admin, require_admin_or_owner,
@@ -62,6 +63,8 @@ pub async fn serve(
6263
worker_id: u16,
6364
leader: Option<Arc<infera_management_core::LeaderElection<Backend>>>,
6465
email_service: Option<Arc<infera_management_core::EmailService>>,
66+
webhook_client: Option<Arc<infera_management_core::WebhookClient>>,
67+
management_identity: Option<Arc<ManagementIdentity>>,
6568
) -> anyhow::Result<()> {
6669
// Create AppState with services
6770
let state = AppState::new(
@@ -71,6 +74,8 @@ pub async fn serve(
7174
worker_id,
7275
leader,
7376
email_service,
77+
webhook_client,
78+
management_identity,
7479
);
7580

7681
let app = create_router_with_state(state);

crates/infera-management-api/src/routes.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ pub fn create_router_with_state(state: AppState) -> axum::Router {
299299
.route("/v1/auth/cli/token", post(cli_auth::cli_token_exchange))
300300
// JWKS endpoints (public, no authentication required)
301301
.route("/.well-known/jwks.json", get(jwks::get_global_jwks))
302+
.route(
303+
"/.well-known/management-jwks.json",
304+
get(jwks::get_management_jwks),
305+
)
302306
.route("/v1/organizations/{org}/jwks.json", get(jwks::get_org_jwks))
303307
.route(
304308
"/v1/organizations/{org}/.well-known/jwks.json",

crates/infera-management-core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ webauthn-rs = { workspace = true }
4949
regex = { workspace = true }
5050
validator = { workspace = true }
5151

52+
# HTTP client (for webhook notifications)
53+
reqwest = { workspace = true }
54+
url = { workspace = true }
55+
5256
# Metrics
5357
metrics = { workspace = true }
5458

0 commit comments

Comments
 (0)