Skip to content

Commit 63b62c8

Browse files
key type to accomodate query via api key
1 parent b15e706 commit 63b62c8

2 files changed

Lines changed: 119 additions & 36 deletions

File tree

src/apikeys.rs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,42 @@ pub struct ApiKeyStore {
3939
pub keys: RwLock<HashMap<String, HashMap<Ulid, ApiKey>>>,
4040
}
4141

42+
/// Type of API key, determining how it can be used.
43+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44+
#[serde(rename_all = "lowercase")]
45+
pub enum KeyType {
46+
/// Used as a substitute for basic auth on ingestion endpoints
47+
Ingestion,
48+
/// Used as a substitute for basic auth on query endpoints (global query access)
49+
Query,
50+
}
51+
4252
#[derive(Debug, Clone, Serialize, Deserialize)]
4353
#[serde(rename_all = "camelCase")]
4454
pub struct ApiKey {
4555
pub key_id: Ulid,
4656
pub api_key: String,
4757
pub key_name: String,
58+
#[serde(default = "default_key_type")]
59+
pub key_type: KeyType,
4860
pub created_by: String,
4961
pub created_at: DateTime<Utc>,
5062
pub modified_at: DateTime<Utc>,
5163
#[serde(default)]
5264
pub tenant: Option<String>,
5365
}
5466

67+
fn default_key_type() -> KeyType {
68+
KeyType::Ingestion
69+
}
70+
5571
/// Request body for creating a new API key
5672
#[derive(Debug, Deserialize)]
5773
#[serde(rename_all = "camelCase")]
5874
pub struct CreateApiKeyRequest {
5975
pub key_name: String,
76+
#[serde(default = "default_key_type")]
77+
pub key_type: KeyType,
6078
}
6179

6280
/// Response for list keys (api_key masked to last 4 chars)
@@ -66,18 +84,25 @@ pub struct ApiKeyListEntry {
6684
pub key_id: Ulid,
6785
pub api_key: String,
6886
pub key_name: String,
87+
pub key_type: KeyType,
6988
pub created_by: String,
7089
pub created_at: DateTime<Utc>,
7190
pub modified_at: DateTime<Utc>,
7291
}
7392

7493
impl ApiKey {
75-
pub fn new(key_name: String, created_by: String, tenant: Option<String>) -> Self {
94+
pub fn new(
95+
key_name: String,
96+
key_type: KeyType,
97+
created_by: String,
98+
tenant: Option<String>,
99+
) -> Self {
76100
let now = Utc::now();
77101
Self {
78102
key_id: Ulid::new(),
79103
api_key: uuid::Uuid::new_v4().to_string(),
80104
key_name,
105+
key_type,
81106
created_by,
82107
created_at: now,
83108
modified_at: now,
@@ -96,6 +121,7 @@ impl ApiKey {
96121
key_id: self.key_id,
97122
api_key: masked,
98123
key_name: self.key_name.clone(),
124+
key_type: self.key_type,
99125
created_by: self.created_by.clone(),
100126
created_at: self.created_at,
101127
modified_at: self.modified_at,
@@ -228,24 +254,24 @@ impl ApiKeyStore {
228254
.ok_or_else(|| ApiKeyError::KeyNotFound(key_id.to_string()))
229255
}
230256

231-
/// Validate an API key for ingestion. Returns true if the key is valid.
257+
/// Validate an API key against a required key type. Returns true if the
258+
/// key is valid AND its type matches the required type.
232259
/// For multi-tenant: checks the key belongs to the specified tenant.
233260
/// For single-tenant: checks the key exists globally.
234-
pub async fn validate_key(&self, api_key_value: &str, tenant_id: &Option<String>) -> bool {
261+
pub async fn validate_key(
262+
&self,
263+
api_key_value: &str,
264+
tenant_id: &Option<String>,
265+
required_type: KeyType,
266+
) -> bool {
235267
let map = self.keys.read().await;
236-
if let Some(tenant_id) = tenant_id {
237-
// Multi-tenant: check keys for the specific tenant
238-
if let Some(tenant_keys) = map.get(tenant_id) {
239-
return tenant_keys.values().any(|k| k.api_key == api_key_value);
240-
}
241-
false
242-
} else {
243-
// Single-tenant: check keys under DEFAULT_TENANT
244-
if let Some(tenant_keys) = map.get(DEFAULT_TENANT) {
245-
return tenant_keys.values().any(|k| k.api_key == api_key_value);
246-
}
247-
false
268+
let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT);
269+
if let Some(tenant_keys) = map.get(tenant) {
270+
return tenant_keys
271+
.values()
272+
.any(|k| k.api_key == api_key_value && k.key_type == required_type);
248273
}
274+
false
249275
}
250276

251277
/// Insert an API key directly into memory (used for sync from prism)

src/handlers/http/middleware.rs

Lines changed: 78 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -156,29 +156,69 @@ where
156156
let mut header_error = None;
157157
let user_and_tenant_id = get_user_and_tenant(&self.action, &mut req, &mut header_error);
158158

159-
// If X-API-KEY header is present for ingestion, short-circuit normal auth
160-
if let Some(api_key) = extract_api_key(&req, &self.action) {
159+
// If X-API-KEY header is present and the action supports API key auth,
160+
// short-circuit the normal auth flow.
161+
if let Some(api_key) = extract_api_key(&req)
162+
&& let Some(required_type) = api_key_type_for_action(&self.action)
163+
{
164+
struct SessionCleanupGuard(Option<Ulid>);
165+
impl Drop for SessionCleanupGuard {
166+
fn drop(&mut self) {
167+
if let Some(sid) = self.0 {
168+
mut_sessions().remove_session(&SessionKey::SessionId(sid));
169+
}
170+
}
171+
}
172+
161173
let suspension = check_suspension(req.request(), self.action);
162174
let tenant_id = req
163175
.headers()
164176
.get(TENANT_ID)
165177
.and_then(|v| v.to_str().ok())
166178
.map(String::from);
179+
180+
// For Query keys, set up a short-lived session with global reader
181+
// permissions so the query handler's per-stream auth passes.
182+
let query_session_id = if required_type == crate::apikeys::KeyType::Query {
183+
let session_id = Ulid::new();
184+
let session_key = SessionKey::SessionId(session_id);
185+
mut_sessions().track_new(
186+
format!("api-key:{session_id}"),
187+
session_key.clone(),
188+
Utc::now() + TimeDelta::minutes(5),
189+
query_api_key_permissions(),
190+
&tenant_id,
191+
);
192+
req.extensions_mut().insert(session_key);
193+
Some(session_id)
194+
} else {
195+
None
196+
};
197+
167198
let fut = self.service.call(req);
168199

169200
return Box::pin(async move {
170-
if let Some(err) = header_error {
171-
return Err(err);
172-
}
173-
if let rbac::Response::Suspended(msg) = suspension {
174-
return Err(ErrorBadRequest(msg));
175-
}
201+
let _guard = SessionCleanupGuard(query_session_id);
202+
let result: Result<ServiceResponse<B>, Error> = async {
203+
if let Some(err) = header_error {
204+
return Err(err);
205+
}
206+
if let rbac::Response::Suspended(msg) = suspension {
207+
return Err(ErrorBadRequest(msg));
208+
}
176209

177-
use crate::apikeys::API_KEYS;
178-
if API_KEYS.validate_key(&api_key, &tenant_id).await {
179-
return fut.await;
210+
use crate::apikeys::API_KEYS;
211+
if API_KEYS
212+
.validate_key(&api_key, &tenant_id, required_type)
213+
.await
214+
{
215+
return fut.await;
216+
}
217+
Err(ErrorUnauthorized("Invalid API key"))
180218
}
181-
Err(ErrorUnauthorized("Invalid API key"))
219+
.await;
220+
221+
result
182222
});
183223
}
184224

@@ -263,18 +303,35 @@ fn extract_kinesis_headers(req: &mut ServiceRequest) {
263303
}
264304
}
265305

266-
/// Extract X-API-KEY header value if present and action is Ingest.
267-
fn extract_api_key(req: &ServiceRequest, action: &Action) -> Option<String> {
268-
if action.eq(&Action::Ingest) {
269-
req.headers()
270-
.get("x-api-key")
271-
.and_then(|v| v.to_str().ok())
272-
.map(String::from)
273-
} else {
274-
None
306+
/// Extract X-API-KEY header value if present (independent of action).
307+
fn extract_api_key(req: &ServiceRequest) -> Option<String> {
308+
req.headers()
309+
.get("x-api-key")
310+
.and_then(|v| v.to_str().ok())
311+
.map(String::from)
312+
}
313+
314+
/// Map an Action to the KeyType required for API key auth on that action.
315+
/// Returns None if the action doesn't support API key auth.
316+
fn api_key_type_for_action(action: &Action) -> Option<crate::apikeys::KeyType> {
317+
use crate::apikeys::KeyType;
318+
match action {
319+
Action::Ingest => Some(KeyType::Ingestion),
320+
Action::Query => Some(KeyType::Query),
321+
_ => None,
275322
}
276323
}
277324

325+
/// Build the set of permissions granted to a Query API key session.
326+
/// Equivalent to the Reader privilege with no resource restriction
327+
/// (global query access across all streams in the tenant).
328+
fn query_api_key_permissions() -> Vec<crate::rbac::role::Permission> {
329+
crate::rbac::role::RoleBuilder::from(&crate::rbac::role::model::DefaultPrivilege::Reader {
330+
resource: None,
331+
})
332+
.build()
333+
}
334+
278335
#[inline]
279336
fn get_user_and_tenant(
280337
action: &Action,

0 commit comments

Comments
 (0)