Skip to content

Commit bd31b0d

Browse files
update: store API keys as a user type in store metadata (#1628)
* store API keys as a third user type in parseable.json * skip apikey type user in listing * remove key type and add roles * fix status code * update lock
1 parent 76ff210 commit bd31b0d

14 files changed

Lines changed: 269 additions & 476 deletions

File tree

src/apikeys.rs

Lines changed: 20 additions & 288 deletions
Original file line numberDiff line numberDiff line change
@@ -16,298 +16,26 @@
1616
*
1717
*/
1818

19-
use std::collections::HashMap;
19+
//! API key primitives.
20+
//!
21+
//! API keys are persisted as a third `UserType` variant (`UserType::ApiKey`)
22+
//! inside `parseable.json`, alongside native and OAuth users. The backing
23+
//! user's permissions are resolved from the `roles` assigned to it (same
24+
//! mechanism as native and OAuth users).
2025
21-
use chrono::{DateTime, Utc};
22-
use once_cell::sync::Lazy;
23-
use serde::{Deserialize, Serialize};
24-
use tokio::sync::RwLock;
25-
use ulid::Ulid;
26+
use std::collections::HashSet;
2627

27-
use crate::{
28-
metastore::metastore_traits::MetastoreObject,
29-
parseable::{DEFAULT_TENANT, PARSEABLE},
30-
storage::object_storage::apikey_json_path,
31-
};
28+
use serde::Deserialize;
3229

33-
pub static API_KEYS: Lazy<ApiKeyStore> = Lazy::new(|| ApiKeyStore {
34-
keys: RwLock::new(HashMap::new()),
35-
});
36-
37-
#[derive(Debug)]
38-
pub struct ApiKeyStore {
39-
pub keys: RwLock<HashMap<String, HashMap<Ulid, ApiKey>>>,
40-
}
41-
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-
52-
#[derive(Debug, Clone, Serialize, Deserialize)]
53-
#[serde(rename_all = "camelCase")]
54-
pub struct ApiKey {
55-
pub key_id: Ulid,
56-
pub api_key: String,
57-
pub key_name: String,
58-
#[serde(default = "default_key_type")]
59-
pub key_type: KeyType,
60-
pub created_by: String,
61-
pub created_at: DateTime<Utc>,
62-
pub modified_at: DateTime<Utc>,
63-
#[serde(default)]
64-
pub tenant: Option<String>,
65-
}
66-
67-
fn default_key_type() -> KeyType {
68-
KeyType::Ingestion
69-
}
70-
71-
/// Request body for creating a new API key
30+
/// Request body for creating a new API key. `roles` is a set of role names
31+
/// that must already exist in the tenant; permissions for the backing user
32+
/// are derived from these roles (same flow as native/OAuth users).
7233
#[derive(Debug, Deserialize)]
7334
#[serde(rename_all = "camelCase")]
7435
pub struct CreateApiKeyRequest {
7536
pub key_name: String,
76-
#[serde(default = "default_key_type")]
77-
pub key_type: KeyType,
78-
}
79-
80-
/// Response for list keys (api_key masked to last 4 chars)
81-
#[derive(Debug, Serialize)]
82-
#[serde(rename_all = "camelCase")]
83-
pub struct ApiKeyListEntry {
84-
pub key_id: Ulid,
85-
pub api_key: String,
86-
pub key_name: String,
87-
pub key_type: KeyType,
88-
pub created_by: String,
89-
pub created_at: DateTime<Utc>,
90-
pub modified_at: DateTime<Utc>,
91-
}
92-
93-
impl ApiKey {
94-
pub fn new(
95-
key_name: String,
96-
key_type: KeyType,
97-
created_by: String,
98-
tenant: Option<String>,
99-
) -> Self {
100-
let now = Utc::now();
101-
Self {
102-
key_id: Ulid::new(),
103-
api_key: uuid::Uuid::new_v4().to_string(),
104-
key_name,
105-
key_type,
106-
created_by,
107-
created_at: now,
108-
modified_at: now,
109-
tenant,
110-
}
111-
}
112-
113-
pub fn to_list_entry(&self) -> ApiKeyListEntry {
114-
let masked = if self.api_key.len() >= 4 {
115-
let last4 = &self.api_key[self.api_key.len() - 4..];
116-
format!("****{last4}")
117-
} else {
118-
"****".to_string()
119-
};
120-
ApiKeyListEntry {
121-
key_id: self.key_id,
122-
api_key: masked,
123-
key_name: self.key_name.clone(),
124-
key_type: self.key_type,
125-
created_by: self.created_by.clone(),
126-
created_at: self.created_at,
127-
modified_at: self.modified_at,
128-
}
129-
}
130-
}
131-
132-
impl MetastoreObject for ApiKey {
133-
fn get_object_path(&self) -> String {
134-
apikey_json_path(&self.key_id, &self.tenant).to_string()
135-
}
136-
137-
fn get_object_id(&self) -> String {
138-
self.key_id.to_string()
139-
}
140-
}
141-
142-
impl ApiKeyStore {
143-
/// Load API keys from object store into memory
144-
pub async fn load(&self) -> anyhow::Result<()> {
145-
let api_keys = PARSEABLE.metastore.get_api_keys().await?;
146-
let mut map = self.keys.write().await;
147-
for (tenant_id, keys) in api_keys {
148-
let inner = keys
149-
.into_iter()
150-
.map(|mut k| {
151-
k.tenant = if tenant_id == DEFAULT_TENANT {
152-
None
153-
} else {
154-
Some(tenant_id.clone())
155-
};
156-
(k.key_id, k)
157-
})
158-
.collect();
159-
map.insert(tenant_id, inner);
160-
}
161-
Ok(())
162-
}
163-
164-
/// Create a new API key
165-
pub async fn create(&self, api_key: ApiKey) -> Result<(), ApiKeyError> {
166-
let tenant = api_key.tenant.as_deref().unwrap_or(DEFAULT_TENANT);
167-
let key_id = api_key.key_id;
168-
169-
// Check duplicate name and reserve the slot under the write lock,
170-
// then drop the lock before the async metastore call so we don't
171-
// hold a global lock across an await.
172-
{
173-
let mut map = self.keys.write().await;
174-
if let Some(tenant_keys) = map.get(tenant)
175-
&& tenant_keys.values().any(|k| k.key_name == api_key.key_name)
176-
{
177-
return Err(ApiKeyError::DuplicateKeyName(api_key.key_name));
178-
}
179-
map.entry(tenant.to_owned())
180-
.or_default()
181-
.insert(key_id, api_key.clone());
182-
}
183-
184-
// Persist to storage without holding the lock. On failure, remove
185-
// the reservation so stale entries don't linger in memory.
186-
if let Err(e) = PARSEABLE
187-
.metastore
188-
.put_api_key(&api_key, &api_key.tenant)
189-
.await
190-
{
191-
let mut map = self.keys.write().await;
192-
if let Some(tenant_keys) = map.get_mut(tenant) {
193-
tenant_keys.remove(&key_id);
194-
}
195-
return Err(e.into());
196-
}
197-
198-
Ok(())
199-
}
200-
201-
/// Delete an API key by key_id
202-
pub async fn delete(
203-
&self,
204-
key_id: &Ulid,
205-
tenant_id: &Option<String>,
206-
) -> Result<ApiKey, ApiKeyError> {
207-
let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT);
208-
209-
// Read the key first without removing
210-
let api_key = {
211-
let map = self.keys.read().await;
212-
let tenant_keys = map
213-
.get(tenant)
214-
.ok_or_else(|| ApiKeyError::KeyNotFound(key_id.to_string()))?;
215-
tenant_keys
216-
.get(key_id)
217-
.cloned()
218-
.ok_or_else(|| ApiKeyError::KeyNotFound(key_id.to_string()))?
219-
};
220-
221-
// Delete from storage first
222-
PARSEABLE
223-
.metastore
224-
.delete_api_key(&api_key, tenant_id)
225-
.await?;
226-
227-
// Remove from memory only after successful storage deletion
228-
{
229-
let mut map = self.keys.write().await;
230-
if let Some(tenant_keys) = map.get_mut(tenant) {
231-
tenant_keys.remove(key_id);
232-
}
233-
}
234-
235-
Ok(api_key)
236-
}
237-
238-
/// List all API keys for a tenant (returns masked entries)
239-
pub async fn list(
240-
&self,
241-
tenant_id: &Option<String>,
242-
) -> Result<Vec<ApiKeyListEntry>, ApiKeyError> {
243-
let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT);
244-
let map = self.keys.read().await;
245-
let entries = if let Some(tenant_keys) = map.get(tenant) {
246-
tenant_keys.values().map(|k| k.to_list_entry()).collect()
247-
} else {
248-
vec![]
249-
};
250-
Ok(entries)
251-
}
252-
253-
/// Get a specific API key by key_id (returns full key)
254-
pub async fn get(
255-
&self,
256-
key_id: &Ulid,
257-
tenant_id: &Option<String>,
258-
) -> Result<ApiKey, ApiKeyError> {
259-
let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT);
260-
let map = self.keys.read().await;
261-
let tenant_keys = map
262-
.get(tenant)
263-
.ok_or_else(|| ApiKeyError::KeyNotFound(key_id.to_string()))?;
264-
tenant_keys
265-
.get(key_id)
266-
.cloned()
267-
.ok_or_else(|| ApiKeyError::KeyNotFound(key_id.to_string()))
268-
}
269-
270-
/// Validate an API key against a required key type. Returns true if the
271-
/// key is valid AND its type matches the required type.
272-
/// For multi-tenant: checks the key belongs to the specified tenant.
273-
/// For single-tenant: checks the key exists globally.
274-
pub async fn validate_key(
275-
&self,
276-
api_key_value: &str,
277-
tenant_id: &Option<String>,
278-
required_type: KeyType,
279-
) -> bool {
280-
let map = self.keys.read().await;
281-
let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT);
282-
if let Some(tenant_keys) = map.get(tenant) {
283-
return tenant_keys
284-
.values()
285-
.any(|k| k.api_key == api_key_value && k.key_type == required_type);
286-
}
287-
false
288-
}
289-
290-
/// Insert an API key directly into memory (used for sync from prism)
291-
pub async fn sync_put(&self, api_key: ApiKey) {
292-
let tenant = api_key
293-
.tenant
294-
.as_deref()
295-
.unwrap_or(DEFAULT_TENANT)
296-
.to_owned();
297-
let mut map = self.keys.write().await;
298-
map.entry(tenant)
299-
.or_default()
300-
.insert(api_key.key_id, api_key);
301-
}
302-
303-
/// Remove an API key from memory (used for sync from prism)
304-
pub async fn sync_delete(&self, key_id: &Ulid, tenant_id: &Option<String>) {
305-
let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT);
306-
let mut map = self.keys.write().await;
307-
if let Some(tenant_keys) = map.get_mut(tenant) {
308-
tenant_keys.remove(key_id);
309-
}
310-
}
37+
#[serde(default)]
38+
pub roles: HashSet<String>,
31139
}
31240

31341
#[derive(Debug, thiserror::Error)]
@@ -322,10 +50,13 @@ pub enum ApiKeyError {
32250
Unauthorized(String),
32351

32452
#[error("{0}")]
325-
MetastoreError(#[from] crate::metastore::MetastoreError),
53+
Storage(#[from] crate::storage::ObjectStorageError),
54+
55+
#[error("{0}")]
56+
Rbac(#[from] crate::handlers::http::rbac::RBACError),
32657

32758
#[error("{0}")]
328-
AnyhowError(#[from] anyhow::Error),
59+
Anyhow(#[from] anyhow::Error),
32960
}
33061

33162
impl actix_web::ResponseError for ApiKeyError {
@@ -334,7 +65,8 @@ impl actix_web::ResponseError for ApiKeyError {
33465
ApiKeyError::KeyNotFound(_) => actix_web::http::StatusCode::NOT_FOUND,
33566
ApiKeyError::DuplicateKeyName(_) => actix_web::http::StatusCode::CONFLICT,
33667
ApiKeyError::Unauthorized(_) => actix_web::http::StatusCode::FORBIDDEN,
337-
ApiKeyError::MetastoreError(_) | ApiKeyError::AnyhowError(_) => {
68+
ApiKeyError::Rbac(err) => actix_web::ResponseError::status_code(err),
69+
ApiKeyError::Storage(_) | ApiKeyError::Anyhow(_) => {
33870
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
33971
}
34072
}

0 commit comments

Comments
 (0)