feat(ui): implement scoped key creation on search UI enable (P5.21.a)

Implements plan §13.21 auth model layer 1 - when search UI is first
enabled for an index, the orchestrator now creates a scoped search-only
key on every Meilisearch node via POST /keys with actions: [search],
indexes scoped. The key is stored in Redis hash with metadata
(primary_uid, rotated_at, generation) for retrieval at request time.

Changes:
- Add imports for MeilisearchClient and mint_scoped_key
- Implement get_or_create_scoped_key to create keys when needed
- Store new keys in Redis via set_search_ui_scoped_key
- Return the scoped key for use in JWT session minting

The scoped key has a hard expiration of scoped_key_max_age_days (60d
default) and will be auto-rotated by the background rotation loop at
scoped_key_rotate_before_expiry_days (30d default) - see P10.5 for
the rotation coordination implementation.

Closes: miroir-uhj.21.1
This commit is contained in:
jedarden 2026-05-24 18:13:16 -04:00
parent ad1c9d011c
commit ecb27e78ff

View file

@ -21,9 +21,11 @@ use crate::error_response::ErrorResponse;
use rust_embed::RustEmbed as Embed;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{debug, info};
use tracing::{debug, info, warn};
use crate::auth::{build_csp_header, jwt_encode, JwtClaims, JwtHeader, KID_PRIMARY};
use crate::routes::indexes::MeilisearchClient;
use crate::scoped_key_rotation::mint_scoped_key;
use super::admin_endpoints::AppState;
@ -331,12 +333,38 @@ async fn get_or_create_scoped_key(
);
}
// TODO: Create new scoped key via Meilisearch API
// For now, return an error indicating the key needs to be created
Err(ErrorResponse::internal_error(format!(
"scoped key for index '{}' needs to be created via background rotation",
index_uid
)))
// Create new scoped key via Meilisearch API (plan §13.21)
info!(index = %index_uid, "creating new scoped search-only key");
let client = MeilisearchClient::new(state.config.node_master_key.clone());
let (new_key, new_uid) = mint_scoped_key(&client, &state.config, index_uid)
.await
.map_err(|e| ErrorResponse::internal_error(format!("failed to mint scoped key: {}", e)))?;
let now_ms = chrono::Utc::now().timestamp_millis();
let scoped_key = SearchUiScopedKey {
index_uid: index_uid.to_string(),
primary_key: new_key.clone(),
primary_uid: new_uid.clone(),
previous_key: None,
previous_uid: None,
rotated_at: now_ms,
generation: 1,
};
// Store in Redis
redis_store
.set_search_ui_scoped_key(&scoped_key)
.map_err(|e| ErrorResponse::internal_error(format!("failed to store scoped key: {}", e)))?;
info!(
index = %index_uid,
uid = %new_uid,
generation = 1,
"created new scoped search-only key"
);
Ok(scoped_key)
}
/// Get per-index search UI configuration (plan §13.21).