use crate::api::models::api_keys::ListApiKeysQuery;
use crate::{
AppState,
api::models::{
api_keys::{ApiKeyCreate, ApiKeyInfoResponse, ApiKeyResponse},
pagination::PaginatedResponse,
users::CurrentUser,
},
auth::permissions::{
can_create_all_resources, can_create_own_resource, can_delete_all_resources, can_delete_own_resource, can_read_all_resources,
can_read_own_resource, is_org_member,
},
db::handlers::{Repository, api_keys::ApiKeyFilter, api_keys::ApiKeys},
db::models::api_keys::ApiKeyCreateDBRequest,
errors::{Error, Result},
types::{ApiKeyId, Operation, Permission, Resource, UserIdOrCurrent},
};
use sqlx_pool_router::PoolProvider;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::Json,
};
use sqlx::Acquire;
#[utoipa::path(
post,
path = "/users/{user_id}/api-keys",
tag = "api_keys",
summary = "Create API key",
description = "Create an API key for the current user or a specified user",
params(
("user_id" = String, Path, description = "User ID (UUID) or 'current' for current user"),
),
responses(
(status = 201, description = "API key created successfully", body = ApiKeyResponse),
(status = 400, description = "Bad request - invalid API key data"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - can only manage own API keys unless admin"),
(status = 500, description = "Internal server error"),
),
security(
("BearerAuth" = []),
("CookieAuth" = []),
("X-Doubleword-User" = [])
)
)]
#[tracing::instrument(skip_all)]
pub async fn create_user_api_key<P: PoolProvider>(
State(state): State<AppState<P>>,
Path(user_id): Path<UserIdOrCurrent>,
current_user: CurrentUser,
Json(data): Json<ApiKeyCreate>,
) -> Result<(StatusCode, Json<ApiKeyResponse>)> {
if data.name.trim().is_empty() {
return Err(Error::BadRequest {
message: "API key name cannot be empty".to_string(),
});
}
let target_user_id = match user_id {
UserIdOrCurrent::Current(_) => current_user.id,
UserIdOrCurrent::Id(uuid) => uuid,
};
let can_create_all = can_create_all_resources(¤t_user, Resource::ApiKeys);
let can_create_own = can_create_own_resource(¤t_user, Resource::ApiKeys, target_user_id);
if !can_create_all && !can_create_own {
let mut conn = state.db.read().acquire().await.map_err(|e| Error::Database(e.into()))?;
let member = is_org_member(¤t_user, target_user_id, &mut conn)
.await
.map_err(Error::Database)?;
if !member {
return Err(Error::InsufficientPermissions {
required: Permission::Any(vec![
Permission::Allow(Resource::ApiKeys, Operation::CreateAll),
Permission::Allow(Resource::ApiKeys, Operation::CreateOwn),
]),
action: Operation::CreateOwn,
resource: format!("API keys for user {target_user_id}"),
});
}
}
if data.member_id.is_some() && !can_create_all {
return Err(Error::InsufficientPermissions {
required: Permission::Allow(Resource::ApiKeys, Operation::CreateAll),
action: Operation::CreateAll,
resource: "API keys with member_id (requires PlatformManager)".to_string(),
});
}
match &data.purpose {
crate::db::models::api_keys::ApiKeyPurpose::Batch | crate::db::models::api_keys::ApiKeyPurpose::Playground => {
return Err(Error::BadRequest {
message:
"Cannot manually create API keys with 'batch' or 'playground' purpose. These are reserved for internal system use."
.to_string(),
});
}
_ => {}
}
let mut pool_conn = state.db.write().acquire().await.map_err(|e| Error::Database(e.into()))?;
let target_is_org = {
let mut org_repo = crate::db::handlers::Organizations::new(&mut pool_conn);
org_repo.exists(target_user_id).await.map_err(Error::Database)?
};
if let Some(member_id) = data.member_id {
if !target_is_org {
return Err(Error::BadRequest {
message: "member_id can only be used when creating keys for an organization".to_string(),
});
}
let mut org_repo = crate::db::handlers::Organizations::new(&mut pool_conn);
let role = org_repo
.get_user_org_role(member_id, target_user_id)
.await
.map_err(Error::Database)?;
if role.is_none() {
return Err(Error::BadRequest {
message: format!("User {member_id} is not a member of organization {target_user_id}"),
});
}
}
let pm_creating_for_other = can_create_all && target_user_id != current_user.id;
let created_by = if target_is_org {
data.member_id.unwrap_or(current_user.id)
} else if pm_creating_for_other {
target_user_id
} else {
current_user.id
};
let mut repo = ApiKeys::new(&mut pool_conn);
let db_request = ApiKeyCreateDBRequest::new(target_user_id, created_by, data);
let api_key = repo.create(&db_request).await?;
Ok((StatusCode::CREATED, Json(ApiKeyResponse::from(api_key))))
}
#[utoipa::path(
get,
path = "/users/{user_id}/api-keys",
tag = "api_keys",
summary = "List API keys",
description = "List API keys for the current user or a specified user",
params(
("user_id" = String, Path, description = "User ID (UUID) or 'current' for current user"),
ListApiKeysQuery
),
responses(
(status = 200, description = "Paginated list of API keys", body = PaginatedResponse<ApiKeyInfoResponse>),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - can only view own API keys unless admin"),
(status = 500, description = "Internal server error"),
),
security(
("BearerAuth" = []),
("CookieAuth" = []),
("X-Doubleword-User" = [])
)
)]
#[tracing::instrument(skip_all)]
pub async fn list_user_api_keys<P: PoolProvider>(
State(state): State<AppState<P>>,
Path(user_id): Path<UserIdOrCurrent>,
Query(query): Query<ListApiKeysQuery>,
current_user: CurrentUser,
) -> Result<Json<PaginatedResponse<ApiKeyInfoResponse>>> {
let target_user_id = match user_id {
UserIdOrCurrent::Current(_) => current_user.id,
UserIdOrCurrent::Id(uuid) => uuid,
};
let can_read_all = can_read_all_resources(¤t_user, Resource::ApiKeys);
let can_read_own = can_read_own_resource(¤t_user, Resource::ApiKeys, target_user_id);
if !can_read_all && !can_read_own {
let mut conn = state.db.read().acquire().await.map_err(|e| Error::Database(e.into()))?;
let member = is_org_member(¤t_user, target_user_id, &mut conn)
.await
.map_err(Error::Database)?;
if !member {
return Err(Error::InsufficientPermissions {
required: Permission::Any(vec![
Permission::Allow(Resource::ApiKeys, Operation::ReadAll),
Permission::Allow(Resource::ApiKeys, Operation::ReadOwn),
]),
action: Operation::ReadOwn,
resource: format!("API keys for user {target_user_id}"),
});
}
}
let skip_created_by_filter = can_read_all;
let mut pool_conn = state.db.read().acquire().await.map_err(|e| Error::Database(e.into()))?;
let mut repo = ApiKeys::new(&mut pool_conn);
let skip = query.pagination.skip();
let limit = query.pagination.limit();
let filter = ApiKeyFilter {
skip,
limit,
user_id: Some(target_user_id),
created_by: if skip_created_by_filter { None } else { Some(current_user.id) },
};
let total_count = repo.count(&filter).await?;
let api_keys = repo.list(&filter).await?;
let data: Vec<ApiKeyInfoResponse> = api_keys.into_iter().map(ApiKeyInfoResponse::from).collect();
Ok(Json(PaginatedResponse::new(data, total_count, skip, limit)))
}
#[utoipa::path(
get,
path = "/users/{user_id}/api-keys/{id}",
tag = "api_keys",
summary = "Get API key",
description = "Get a specific API key for the current user or a specified user",
params(
("user_id" = String, Path, description = "User ID (UUID) or 'current' for current user"),
("id" = uuid::Uuid, Path, description = "API key ID to retrieve"),
),
responses(
(status = 200, description = "API key information", body = ApiKeyInfoResponse),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - can only view own API keys unless admin"),
(status = 404, description = "API key not found"),
(status = 500, description = "Internal server error"),
),
security(
("BearerAuth" = []),
("CookieAuth" = []),
("X-Doubleword-User" = [])
)
)]
#[tracing::instrument(skip_all)]
pub async fn get_user_api_key<P: PoolProvider>(
State(state): State<AppState<P>>,
Path((user_id, api_key_id)): Path<(UserIdOrCurrent, ApiKeyId)>,
current_user: CurrentUser,
) -> Result<Json<ApiKeyInfoResponse>> {
let target_user_id = match user_id {
UserIdOrCurrent::Current(_) => current_user.id,
UserIdOrCurrent::Id(uuid) => uuid,
};
let can_read_all = can_read_all_resources(¤t_user, Resource::ApiKeys);
let can_read_own = can_read_own_resource(¤t_user, Resource::ApiKeys, target_user_id);
if !can_read_all && !can_read_own {
let mut conn = state.db.read().acquire().await.map_err(|e| Error::Database(e.into()))?;
let member = is_org_member(¤t_user, target_user_id, &mut conn)
.await
.map_err(Error::Database)?;
if !member {
return Err(Error::InsufficientPermissions {
required: Permission::Any(vec![
Permission::Allow(Resource::ApiKeys, Operation::ReadAll),
Permission::Allow(Resource::ApiKeys, Operation::ReadOwn),
]),
action: Operation::ReadOwn,
resource: format!("API keys for user {target_user_id}"),
});
}
}
let skip_created_by_filter = can_read_all;
let mut pool_conn = state.db.read().acquire().await.map_err(|e| Error::Database(e.into()))?;
let mut repo = ApiKeys::new(&mut pool_conn);
let api_key = repo
.get_by_id(api_key_id)
.await?
.filter(|key| key.user_id == target_user_id)
.filter(|key| skip_created_by_filter || key.created_by == current_user.id)
.ok_or_else(|| Error::NotFound {
resource: "API key".to_string(),
id: api_key_id.to_string(),
})?;
Ok(Json(ApiKeyInfoResponse::from(api_key)))
}
#[utoipa::path(
delete,
path = "/users/{user_id}/api-keys/{id}",
tag = "api_keys",
summary = "Delete API key",
description = "Delete a specific API key for the current user or a specified user",
params(
("user_id" = String, Path, description = "User ID (UUID) or 'current' for current user"),
("id" = uuid::Uuid, Path, description = "API key ID to delete"),
),
responses(
(status = 204, description = "API key deleted successfully"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - can only delete own API keys unless admin"),
(status = 404, description = "API key not found"),
(status = 500, description = "Internal server error"),
),
security(
("BearerAuth" = []),
("CookieAuth" = []),
("X-Doubleword-User" = [])
)
)]
#[tracing::instrument(skip_all)]
pub async fn delete_user_api_key<P: PoolProvider>(
State(state): State<AppState<P>>,
Path((user_id, api_key_id)): Path<(UserIdOrCurrent, ApiKeyId)>,
current_user: CurrentUser,
) -> Result<StatusCode> {
let target_user_id = match user_id {
UserIdOrCurrent::Current(_) => current_user.id,
UserIdOrCurrent::Id(uuid) => uuid,
};
let can_delete_all = can_delete_all_resources(¤t_user, Resource::ApiKeys);
let can_delete_own = can_delete_own_resource(¤t_user, Resource::ApiKeys, target_user_id);
if !can_delete_all && !can_delete_own {
let mut conn = state.db.read().acquire().await.map_err(|e| Error::Database(e.into()))?;
let member = is_org_member(¤t_user, target_user_id, &mut conn)
.await
.map_err(Error::Database)?;
if !member {
return Err(Error::InsufficientPermissions {
required: Permission::Any(vec![
Permission::Allow(Resource::ApiKeys, Operation::DeleteAll),
Permission::Allow(Resource::ApiKeys, Operation::DeleteOwn),
]),
action: Operation::DeleteOwn,
resource: format!("API keys for user {target_user_id}"),
});
}
}
let skip_created_by_filter = can_delete_all;
let mut tx = state.db.write().begin().await.map_err(|e| Error::Database(e.into()))?;
let mut repo = ApiKeys::new(tx.acquire().await.map_err(|e| Error::Database(e.into()))?);
repo.get_by_id(api_key_id)
.await?
.filter(|key| key.user_id == target_user_id)
.filter(|key| skip_created_by_filter || key.created_by == current_user.id)
.ok_or_else(|| Error::NotFound {
resource: "API key".to_string(),
id: api_key_id.to_string(),
})?;
repo.delete(api_key_id).await?;
tx.commit().await.map_err(|e| Error::Database(e.into()))?;
Ok(StatusCode::NO_CONTENT)
}
#[cfg(test)]
mod tests {
use crate::api::models::api_keys::{ApiKeyInfoResponse, ApiKeyResponse};
use crate::api::models::pagination::PaginatedResponse;
use crate::api::models::users::Role;
use crate::test::utils::*;
use serde_json::json;
use sqlx::PgPool;
#[sqlx::test]
#[test_log::test]
async fn test_create_api_key_for_self(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user.id, group.id).await;
let api_key_data = json!({
"name": "Test API Key",
"description": "A test API key",
"purpose": "realtime"
});
let response = app
.post("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.json(&api_key_data)
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let api_key: ApiKeyResponse = response.json();
assert_eq!(api_key.name, "Test API Key");
assert_eq!(api_key.description, Some("A test API key".to_string()));
assert!(api_key.key.starts_with("sk-"));
}
#[sqlx::test]
#[test_log::test]
async fn test_create_api_key_for_other_user_as_admin(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let admin_user = create_test_admin_user(&pool, Role::PlatformManager).await;
let regular_user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, regular_user.id, group.id).await;
let api_key_data = json!({
"name": "Admin Created Key",
"description": "Created by admin for user",
"purpose": "realtime"
});
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", regular_user.id))
.add_header(&add_auth_headers(&admin_user)[0].0, &add_auth_headers(&admin_user)[0].1)
.add_header(&add_auth_headers(&admin_user)[1].0, &add_auth_headers(&admin_user)[1].1)
.json(&api_key_data)
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let api_key: ApiKeyResponse = response.json();
assert_eq!(api_key.name, "Admin Created Key");
assert!(api_key.key.starts_with("sk-"));
}
#[sqlx::test]
#[test_log::test]
async fn test_create_api_key_for_other_user_as_non_admin_forbidden(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user1 = create_test_user(&pool, Role::StandardUser).await;
let user2 = create_test_user(&pool, Role::StandardUser).await;
let api_key_data = json!({
"name": "Forbidden Key",
"description": "This should not work",
"purpose": "realtime"
});
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", user2.id))
.add_header(&add_auth_headers(&user1)[0].0, &add_auth_headers(&user1)[0].1)
.add_header(&add_auth_headers(&user1)[1].0, &add_auth_headers(&user1)[1].1)
.json(&api_key_data)
.await;
response.assert_status_forbidden();
}
#[sqlx::test]
#[test_log::test]
async fn test_list_user_api_keys(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user.id, group.id).await;
let api_key = create_test_api_key_for_user(&pool, user.id).await;
let response = app
.get("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 1);
assert_eq!(paginated.total_count, 1);
assert_eq!(paginated.data[0].name, api_key.name);
}
#[sqlx::test]
#[test_log::test]
async fn test_list_user_api_keys_with_pagination_query_params(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user.id, group.id).await;
for i in 1..=5 {
let api_key_data = json!({
"name": format!("Test API Key {}", i),
"description": format!("Description for key {}", i),
"purpose": "realtime"
});
app.post("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.json(&api_key_data)
.await
.assert_status(axum::http::StatusCode::CREATED);
}
let response = app
.get("/admin/api/v1/users/current/api-keys?skip=1&limit=2")
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 2, "Should return exactly 2 items with limit=2");
assert_eq!(paginated.total_count, 5, "Total count should be 5");
assert_eq!(paginated.skip, 1);
assert_eq!(paginated.limit, 2);
let response = app
.get("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 5, "Should return all items with default pagination");
assert_eq!(paginated.total_count, 5);
let response = app
.get("/admin/api/v1/users/current/api-keys?limit=9999")
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 5, "Should return all items even with large limit");
assert_eq!(paginated.total_count, 5);
}
#[sqlx::test]
#[test_log::test]
async fn test_delete_user_api_key_for_self(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user.id, group.id).await;
let api_key = create_test_api_key_for_user(&pool, user.id).await;
let response = app
.delete(&format!("/admin/api/v1/users/current/api-keys/{}", api_key.id))
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
response.assert_status(axum::http::StatusCode::NO_CONTENT);
let list_response = app
.get("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
list_response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = list_response.json();
assert_eq!(paginated.data.len(), 0);
assert_eq!(paginated.total_count, 0);
}
#[sqlx::test]
#[test_log::test]
async fn test_delete_user_api_key_for_other_user_as_admin(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let admin_user = create_test_admin_user(&pool, Role::PlatformManager).await;
let regular_user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, regular_user.id, group.id).await;
let api_key = create_test_api_key_for_user(&pool, regular_user.id).await;
let response = app
.delete(&format!("/admin/api/v1/users/{}/api-keys/{}", regular_user.id, api_key.id))
.add_header(&add_auth_headers(&admin_user)[0].0, &add_auth_headers(&admin_user)[0].1)
.add_header(&add_auth_headers(&admin_user)[1].0, &add_auth_headers(&admin_user)[1].1)
.await;
response.assert_status(axum::http::StatusCode::NO_CONTENT);
let list_response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", regular_user.id))
.add_header(&add_auth_headers(&admin_user)[0].0, &add_auth_headers(&admin_user)[0].1)
.add_header(&add_auth_headers(&admin_user)[1].0, &add_auth_headers(&admin_user)[1].1)
.await;
list_response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = list_response.json();
assert_eq!(paginated.data.len(), 0);
assert_eq!(paginated.total_count, 0);
}
#[sqlx::test]
#[test_log::test]
async fn test_delete_user_api_key_for_other_user_as_non_admin_forbidden(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user1 = create_test_user(&pool, Role::StandardUser).await;
let user2 = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user2.id, group.id).await;
let api_key = create_test_api_key_for_user(&pool, user2.id).await;
let response = app
.delete(&format!("/admin/api/v1/users/{}/api-keys/{}", user2.id, api_key.id))
.add_header(&add_auth_headers(&user1)[0].0, &add_auth_headers(&user1)[0].1)
.add_header(&add_auth_headers(&user1)[1].0, &add_auth_headers(&user1)[1].1)
.await;
response.assert_status_forbidden();
let list_response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", user2.id))
.add_header(&add_auth_headers(&user2)[0].0, &add_auth_headers(&user2)[0].1)
.add_header(&add_auth_headers(&user2)[1].0, &add_auth_headers(&user2)[1].1)
.await;
list_response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = list_response.json();
assert_eq!(paginated.data.len(), 1);
assert_eq!(paginated.total_count, 1);
}
#[sqlx::test]
#[test_log::test]
async fn test_delete_nonexistent_api_key_returns_not_found(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user.id, group.id).await;
let fake_api_key_id = uuid::Uuid::new_v4();
let response = app
.delete(&format!("/admin/api/v1/users/current/api-keys/{fake_api_key_id}"))
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
response.assert_status_not_found();
}
#[sqlx::test]
#[test_log::test]
async fn test_delete_api_key_belonging_to_different_user_returns_not_found(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user1 = create_test_user(&pool, Role::StandardUser).await;
let user2 = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user1.id, group.id).await;
add_user_to_group(&pool, user2.id, group.id).await;
let api_key = create_test_api_key_for_user(&pool, user2.id).await;
let response = app
.delete(&format!("/admin/api/v1/users/current/api-keys/{}", api_key.id))
.add_header(&add_auth_headers(&user1)[0].0, &add_auth_headers(&user1)[0].1)
.add_header(&add_auth_headers(&user1)[1].0, &add_auth_headers(&user1)[1].1)
.await;
response.assert_status_not_found();
}
#[sqlx::test]
#[test_log::test]
async fn test_list_api_keys_for_other_user_as_admin(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let admin_user = create_test_admin_user(&pool, Role::PlatformManager).await;
let regular_user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, regular_user.id, group.id).await;
let api_key1 = create_test_api_key_for_user(&pool, regular_user.id).await;
let api_key2 = create_test_api_key_for_user(&pool, regular_user.id).await;
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", regular_user.id))
.add_header(&add_auth_headers(&admin_user)[0].0, &add_auth_headers(&admin_user)[0].1)
.add_header(&add_auth_headers(&admin_user)[1].0, &add_auth_headers(&admin_user)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 2);
assert_eq!(paginated.total_count, 2);
let returned_ids: Vec<_> = paginated.data.iter().map(|k| k.id).collect();
assert!(returned_ids.contains(&api_key1.id));
assert!(returned_ids.contains(&api_key2.id));
}
#[sqlx::test]
#[test_log::test]
async fn test_list_api_keys_for_other_user_as_non_admin_forbidden(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user1 = create_test_user(&pool, Role::StandardUser).await;
let user2 = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user2.id, group.id).await;
create_test_api_key_for_user(&pool, user2.id).await;
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", user2.id))
.add_header(&add_auth_headers(&user1)[0].0, &add_auth_headers(&user1)[0].1)
.add_header(&add_auth_headers(&user1)[1].0, &add_auth_headers(&user1)[1].1)
.await;
response.assert_status_forbidden();
}
#[sqlx::test]
#[test_log::test]
async fn test_get_api_key_for_other_user_as_admin(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let admin_user = create_test_admin_user(&pool, Role::PlatformManager).await;
let regular_user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, regular_user.id, group.id).await;
let api_key = create_test_api_key_for_user(&pool, regular_user.id).await;
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys/{}", regular_user.id, api_key.id))
.add_header(&add_auth_headers(&admin_user)[0].0, &add_auth_headers(&admin_user)[0].1)
.add_header(&add_auth_headers(&admin_user)[1].0, &add_auth_headers(&admin_user)[1].1)
.await;
response.assert_status_ok();
let returned_key: ApiKeyInfoResponse = response.json();
assert_eq!(returned_key.id, api_key.id);
assert_eq!(returned_key.name, api_key.name);
}
#[sqlx::test]
#[test_log::test]
async fn test_get_api_key_for_other_user_as_non_admin_forbidden(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user1 = create_test_user(&pool, Role::StandardUser).await;
let user2 = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user2.id, group.id).await;
let api_key = create_test_api_key_for_user(&pool, user2.id).await;
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys/{}", user2.id, api_key.id))
.add_header(&add_auth_headers(&user1)[0].0, &add_auth_headers(&user1)[0].1)
.add_header(&add_auth_headers(&user1)[1].0, &add_auth_headers(&user1)[1].1)
.await;
response.assert_status_forbidden();
}
#[sqlx::test]
#[test_log::test]
async fn test_request_viewer_api_key_permissions(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let request_viewer = create_test_user(&pool, Role::RequestViewer).await;
let other_user = create_test_user(&pool, Role::StandardUser).await;
let api_key_data = json!({
"name": "RequestViewer Key",
"description": "Should work - StandardUser can manage own keys",
"purpose": "realtime"
});
let response = app
.post("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&request_viewer)[0].0, &add_auth_headers(&request_viewer)[0].1)
.add_header(&add_auth_headers(&request_viewer)[1].0, &add_auth_headers(&request_viewer)[1].1)
.json(&api_key_data)
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let response = app
.get("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&request_viewer)[0].0, &add_auth_headers(&request_viewer)[0].1)
.add_header(&add_auth_headers(&request_viewer)[1].0, &add_auth_headers(&request_viewer)[1].1)
.await;
response.assert_status_ok();
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", other_user.id))
.add_header(&add_auth_headers(&request_viewer)[0].0, &add_auth_headers(&request_viewer)[0].1)
.add_header(&add_auth_headers(&request_viewer)[1].0, &add_auth_headers(&request_viewer)[1].1)
.await;
response.assert_status_forbidden();
}
#[sqlx::test]
#[test_log::test]
async fn test_multi_role_user_api_key_permissions(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let multi_role_user = create_test_user_with_roles(&pool, vec![Role::StandardUser, Role::RequestViewer]).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, multi_role_user.id, group.id).await;
let api_key_data = json!({
"name": "Multi Role Key",
"description": "Should work due to StandardUser role",
"purpose": "realtime"
});
let response = app
.post("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&multi_role_user)[0].0, &add_auth_headers(&multi_role_user)[0].1)
.add_header(&add_auth_headers(&multi_role_user)[1].0, &add_auth_headers(&multi_role_user)[1].1)
.json(&api_key_data)
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let response = app
.get("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&multi_role_user)[0].0, &add_auth_headers(&multi_role_user)[0].1)
.add_header(&add_auth_headers(&multi_role_user)[1].0, &add_auth_headers(&multi_role_user)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 1);
assert_eq!(paginated.total_count, 1);
assert_eq!(paginated.data[0].name, "Multi Role Key");
let api_key_id = paginated.data[0].id;
let response = app
.get(&format!("/admin/api/v1/users/current/api-keys/{api_key_id}"))
.add_header(&add_auth_headers(&multi_role_user)[0].0, &add_auth_headers(&multi_role_user)[0].1)
.add_header(&add_auth_headers(&multi_role_user)[1].0, &add_auth_headers(&multi_role_user)[1].1)
.await;
response.assert_status_ok();
let response = app
.delete(&format!("/admin/api/v1/users/current/api-keys/{api_key_id}"))
.add_header(&add_auth_headers(&multi_role_user)[0].0, &add_auth_headers(&multi_role_user)[0].1)
.add_header(&add_auth_headers(&multi_role_user)[1].0, &add_auth_headers(&multi_role_user)[1].1)
.await;
response.assert_status(axum::http::StatusCode::NO_CONTENT);
}
#[sqlx::test]
#[test_log::test]
async fn test_platform_manager_full_api_key_access(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let platform_manager = create_test_user(&pool, Role::PlatformManager).await;
let standard_user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, standard_user.id, group.id).await;
let api_key_data = json!({
"name": "Manager Created Key",
"description": "Created by platform manager",
"purpose": "realtime"
});
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", standard_user.id))
.add_header(&add_auth_headers(&platform_manager)[0].0, &add_auth_headers(&platform_manager)[0].1)
.add_header(&add_auth_headers(&platform_manager)[1].0, &add_auth_headers(&platform_manager)[1].1)
.json(&api_key_data)
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", standard_user.id))
.add_header(&add_auth_headers(&platform_manager)[0].0, &add_auth_headers(&platform_manager)[0].1)
.add_header(&add_auth_headers(&platform_manager)[1].0, &add_auth_headers(&platform_manager)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 1);
assert_eq!(paginated.total_count, 1);
let api_key_id = paginated.data[0].id;
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys/{}", standard_user.id, api_key_id))
.add_header(&add_auth_headers(&platform_manager)[0].0, &add_auth_headers(&platform_manager)[0].1)
.add_header(&add_auth_headers(&platform_manager)[1].0, &add_auth_headers(&platform_manager)[1].1)
.await;
response.assert_status_ok();
let response = app
.get("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&standard_user)[0].0, &add_auth_headers(&standard_user)[0].1)
.add_header(&add_auth_headers(&standard_user)[1].0, &add_auth_headers(&standard_user)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 1);
assert_eq!(paginated.data[0].name, "Manager Created Key");
let response = app
.delete(&format!("/admin/api/v1/users/{}/api-keys/{}", standard_user.id, api_key_id))
.add_header(&add_auth_headers(&platform_manager)[0].0, &add_auth_headers(&platform_manager)[0].1)
.add_header(&add_auth_headers(&platform_manager)[1].0, &add_auth_headers(&platform_manager)[1].1)
.await;
response.assert_status(axum::http::StatusCode::NO_CONTENT);
}
#[sqlx::test]
#[test_log::test]
async fn test_api_key_isolation_between_users(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user1 = create_test_user(&pool, Role::StandardUser).await;
let user2 = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user1.id, group.id).await;
add_user_to_group(&pool, user2.id, group.id).await;
let api_key1 = create_test_api_key_for_user(&pool, user1.id).await;
let api_key2 = create_test_api_key_for_user(&pool, user2.id).await;
let response = app
.get("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&user1)[0].0, &add_auth_headers(&user1)[0].1)
.add_header(&add_auth_headers(&user1)[1].0, &add_auth_headers(&user1)[1].1)
.await;
response.assert_status_ok();
let user1_paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(user1_paginated.data.len(), 1);
assert_eq!(user1_paginated.total_count, 1);
assert_eq!(user1_paginated.data[0].id, api_key1.id);
let response = app
.get("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&user2)[0].0, &add_auth_headers(&user2)[0].1)
.add_header(&add_auth_headers(&user2)[1].0, &add_auth_headers(&user2)[1].1)
.await;
response.assert_status_ok();
let user2_paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(user2_paginated.data.len(), 1);
assert_eq!(user2_paginated.total_count, 1);
assert_eq!(user2_paginated.data[0].id, api_key2.id);
let response = app
.get(&format!("/admin/api/v1/users/current/api-keys/{}", api_key2.id))
.add_header(&add_auth_headers(&user1)[0].0, &add_auth_headers(&user1)[0].1)
.add_header(&add_auth_headers(&user1)[1].0, &add_auth_headers(&user1)[1].1)
.await;
response.assert_status_not_found(); }
#[sqlx::test]
#[test_log::test]
async fn test_error_messages_are_user_friendly(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user1 = create_test_user(&pool, Role::StandardUser).await;
let user2 = create_test_user(&pool, Role::StandardUser).await;
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", user2.id))
.add_header(&add_auth_headers(&user1)[0].0, &add_auth_headers(&user1)[0].1)
.add_header(&add_auth_headers(&user1)[1].0, &add_auth_headers(&user1)[1].1)
.await;
response.assert_status_forbidden();
let body = response.text();
assert!(body.contains("Insufficient permissions to Read"));
assert!(!body.contains("ReadAll"));
assert!(!body.contains("ReadOwn"));
}
#[sqlx::test]
#[test_log::test]
async fn test_get_specific_api_key_for_self(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user.id, group.id).await;
let api_key = create_test_api_key_for_user(&pool, user.id).await;
let response = app
.get(&format!("/admin/api/v1/users/current/api-keys/{}", api_key.id))
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
response.assert_status_ok();
let returned_key: ApiKeyInfoResponse = response.json();
assert_eq!(returned_key.id, api_key.id);
assert_eq!(returned_key.name, api_key.name);
assert_eq!(returned_key.description, api_key.description);
}
#[sqlx::test]
#[test_log::test]
async fn test_api_key_creation_returns_key_value_only_once(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let user = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, user.id, group.id).await;
let api_key_data = json!({
"name": "Test Key for Security",
"description": "Testing key exposure",
"purpose": "realtime"
});
let create_response = app
.post("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.json(&api_key_data)
.await;
create_response.assert_status(axum::http::StatusCode::CREATED);
let created_key: ApiKeyResponse = create_response.json();
assert!(created_key.key.starts_with("sk-"));
assert!(created_key.key.len() > 10);
let list_response = app
.get("/admin/api/v1/users/current/api-keys")
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
list_response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = list_response.json();
assert_eq!(paginated.data.len(), 1);
assert_eq!(paginated.total_count, 1);
let get_response = app
.get(&format!("/admin/api/v1/users/current/api-keys/{}", created_key.id))
.add_header(&add_auth_headers(&user)[0].0, &add_auth_headers(&user)[0].1)
.add_header(&add_auth_headers(&user)[1].0, &add_auth_headers(&user)[1].1)
.await;
get_response.assert_status_ok();
let retrieved_key: ApiKeyInfoResponse = get_response.json();
assert_eq!(retrieved_key.id, created_key.id);
assert_eq!(retrieved_key.name, created_key.name);
}
#[sqlx::test]
#[test_log::test]
async fn test_org_member_can_create_api_key(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let bob = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
add_org_member(&pool, org.id, bob.id, "member").await;
let api_key_data = json!({
"name": "Bob Org Key",
"description": "Created by member",
"purpose": "realtime"
});
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&bob)[0].0, &add_auth_headers(&bob)[0].1)
.add_header(&add_auth_headers(&bob)[1].0, &add_auth_headers(&bob)[1].1)
.json(&api_key_data)
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let api_key: ApiKeyResponse = response.json();
assert_eq!(api_key.name, "Bob Org Key");
assert!(api_key.key.starts_with("sk-"));
}
#[sqlx::test]
#[test_log::test]
async fn test_non_member_cannot_create_org_api_key(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let outsider = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
let api_key_data = json!({
"name": "Outsider Key",
"purpose": "realtime"
});
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&outsider)[0].0, &add_auth_headers(&outsider)[0].1)
.add_header(&add_auth_headers(&outsider)[1].0, &add_auth_headers(&outsider)[1].1)
.json(&api_key_data)
.await;
response.assert_status_forbidden();
}
#[sqlx::test]
#[test_log::test]
async fn test_org_member_list_only_own_keys(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let bob = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
add_org_member(&pool, org.id, bob.id, "member").await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&alice)[0].0, &add_auth_headers(&alice)[0].1)
.add_header(&add_auth_headers(&alice)[1].0, &add_auth_headers(&alice)[1].1)
.json(&json!({"name": "Alice Key", "purpose": "realtime"}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&bob)[0].0, &add_auth_headers(&bob)[0].1)
.add_header(&add_auth_headers(&bob)[1].0, &add_auth_headers(&bob)[1].1)
.json(&json!({"name": "Bob Key", "purpose": "realtime"}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&alice)[0].0, &add_auth_headers(&alice)[0].1)
.add_header(&add_auth_headers(&alice)[1].0, &add_auth_headers(&alice)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 1);
assert_eq!(paginated.data[0].name, "Alice Key");
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&bob)[0].0, &add_auth_headers(&bob)[0].1)
.add_header(&add_auth_headers(&bob)[1].0, &add_auth_headers(&bob)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 1);
assert_eq!(paginated.data[0].name, "Bob Key");
}
#[sqlx::test]
#[test_log::test]
async fn test_org_member_cannot_get_other_members_key(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let bob = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
add_org_member(&pool, org.id, bob.id, "member").await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&bob)[0].0, &add_auth_headers(&bob)[0].1)
.add_header(&add_auth_headers(&bob)[1].0, &add_auth_headers(&bob)[1].1)
.json(&json!({"name": "Bob Key", "purpose": "realtime"}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let bob_key: ApiKeyResponse = response.json();
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys/{}", org.id, bob_key.id))
.add_header(&add_auth_headers(&alice)[0].0, &add_auth_headers(&alice)[0].1)
.add_header(&add_auth_headers(&alice)[1].0, &add_auth_headers(&alice)[1].1)
.await;
response.assert_status_not_found();
}
#[sqlx::test]
#[test_log::test]
async fn test_org_member_cannot_delete_other_members_key(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let bob = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
add_org_member(&pool, org.id, bob.id, "member").await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&bob)[0].0, &add_auth_headers(&bob)[0].1)
.add_header(&add_auth_headers(&bob)[1].0, &add_auth_headers(&bob)[1].1)
.json(&json!({"name": "Bob Key", "purpose": "realtime"}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let bob_key: ApiKeyResponse = response.json();
let response = app
.delete(&format!("/admin/api/v1/users/{}/api-keys/{}", org.id, bob_key.id))
.add_header(&add_auth_headers(&alice)[0].0, &add_auth_headers(&alice)[0].1)
.add_header(&add_auth_headers(&alice)[1].0, &add_auth_headers(&alice)[1].1)
.await;
response.assert_status_not_found();
}
#[sqlx::test]
#[test_log::test]
async fn test_org_member_can_delete_own_key(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let bob = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
add_org_member(&pool, org.id, bob.id, "member").await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&bob)[0].0, &add_auth_headers(&bob)[0].1)
.add_header(&add_auth_headers(&bob)[1].0, &add_auth_headers(&bob)[1].1)
.json(&json!({"name": "Bob Key", "purpose": "realtime"}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let bob_key: ApiKeyResponse = response.json();
let response = app
.delete(&format!("/admin/api/v1/users/{}/api-keys/{}", org.id, bob_key.id))
.add_header(&add_auth_headers(&bob)[0].0, &add_auth_headers(&bob)[0].1)
.add_header(&add_auth_headers(&bob)[1].0, &add_auth_headers(&bob)[1].1)
.await;
response.assert_status(axum::http::StatusCode::NO_CONTENT);
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&bob)[0].0, &add_auth_headers(&bob)[0].1)
.add_header(&add_auth_headers(&bob)[1].0, &add_auth_headers(&bob)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 0);
}
#[sqlx::test]
#[test_log::test]
async fn test_platform_manager_sees_all_org_keys(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let bob = create_test_user(&pool, Role::StandardUser).await;
let pm = create_test_admin_user(&pool, Role::PlatformManager).await;
let org = create_test_org(&pool, alice.id).await;
add_org_member(&pool, org.id, bob.id, "member").await;
app.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&alice)[0].0, &add_auth_headers(&alice)[0].1)
.add_header(&add_auth_headers(&alice)[1].0, &add_auth_headers(&alice)[1].1)
.json(&json!({"name": "Alice Key", "purpose": "realtime"}))
.await
.assert_status(axum::http::StatusCode::CREATED);
app.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&bob)[0].0, &add_auth_headers(&bob)[0].1)
.add_header(&add_auth_headers(&bob)[1].0, &add_auth_headers(&bob)[1].1)
.json(&json!({"name": "Bob Key", "purpose": "realtime"}))
.await
.assert_status(axum::http::StatusCode::CREATED);
let response = app
.get(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&pm)[0].0, &add_auth_headers(&pm)[0].1)
.add_header(&add_auth_headers(&pm)[1].0, &add_auth_headers(&pm)[1].1)
.await;
response.assert_status_ok();
let paginated: PaginatedResponse<ApiKeyInfoResponse> = response.json();
assert_eq!(paginated.data.len(), 2);
}
#[sqlx::test]
#[test_log::test]
async fn test_org_member_key_created_by_is_member_not_org(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&alice)[0].0, &add_auth_headers(&alice)[0].1)
.add_header(&add_auth_headers(&alice)[1].0, &add_auth_headers(&alice)[1].1)
.json(&json!({"name": "Alice Org Key", "purpose": "realtime"}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let key: ApiKeyResponse = response.json();
assert_eq!(key.created_by, alice.id, "created_by should be the member, not the org");
assert_eq!(key.user_id, org.id, "user_id should be the org");
}
#[sqlx::test]
#[test_log::test]
async fn test_pm_org_member_key_created_by_is_pm_not_org(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let pm = create_test_admin_user(&pool, Role::PlatformManager).await;
let org = create_test_org(&pool, pm.id).await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&pm)[0].0, &add_auth_headers(&pm)[0].1)
.add_header(&add_auth_headers(&pm)[1].0, &add_auth_headers(&pm)[1].1)
.json(&json!({"name": "PM Org Key", "purpose": "realtime"}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let key: ApiKeyResponse = response.json();
assert_eq!(key.created_by, pm.id, "created_by should be the PM, not the org");
assert_eq!(key.user_id, org.id, "user_id should be the org");
}
#[sqlx::test]
#[test_log::test]
async fn test_pm_creates_org_key_with_member_id(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let pm = create_test_admin_user(&pool, Role::PlatformManager).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&pm)[0].0, &add_auth_headers(&pm)[0].1)
.add_header(&add_auth_headers(&pm)[1].0, &add_auth_headers(&pm)[1].1)
.json(&json!({"name": "Attributed Key", "purpose": "realtime", "member_id": alice.id}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let key: ApiKeyResponse = response.json();
assert_eq!(key.created_by, alice.id, "created_by should be the specified member_id");
assert_eq!(key.user_id, org.id, "user_id should be the org");
}
#[sqlx::test]
#[test_log::test]
async fn test_pm_member_id_rejected_for_non_org_target(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let pm = create_test_admin_user(&pool, Role::PlatformManager).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, alice.id, group.id).await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", alice.id))
.add_header(&add_auth_headers(&pm)[0].0, &add_auth_headers(&pm)[0].1)
.add_header(&add_auth_headers(&pm)[1].0, &add_auth_headers(&pm)[1].1)
.json(&json!({"name": "Bad Key", "purpose": "realtime", "member_id": pm.id}))
.await;
response.assert_status_bad_request();
}
#[sqlx::test]
#[test_log::test]
async fn test_pm_member_id_rejected_for_non_member(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let pm = create_test_admin_user(&pool, Role::PlatformManager).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let outsider = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&pm)[0].0, &add_auth_headers(&pm)[0].1)
.add_header(&add_auth_headers(&pm)[1].0, &add_auth_headers(&pm)[1].1)
.json(&json!({"name": "Bad Key", "purpose": "realtime", "member_id": outsider.id}))
.await;
response.assert_status_bad_request();
}
#[sqlx::test]
#[test_log::test]
async fn test_non_pm_cannot_use_member_id(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let bob = create_test_user(&pool, Role::StandardUser).await;
let org = create_test_org(&pool, alice.id).await;
add_org_member(&pool, org.id, bob.id, "member").await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", org.id))
.add_header(&add_auth_headers(&alice)[0].0, &add_auth_headers(&alice)[0].1)
.add_header(&add_auth_headers(&alice)[1].0, &add_auth_headers(&alice)[1].1)
.json(&json!({"name": "Bad Key", "purpose": "realtime", "member_id": bob.id}))
.await;
response.assert_status_forbidden();
}
#[sqlx::test]
#[test_log::test]
async fn test_pm_creates_key_for_individual_user_created_by_is_target(pool: PgPool) {
let (app, _bg_services) = create_test_app(pool.clone(), false).await;
let pm = create_test_admin_user(&pool, Role::PlatformManager).await;
let alice = create_test_user(&pool, Role::StandardUser).await;
let group = create_test_group(&pool).await;
add_user_to_group(&pool, alice.id, group.id).await;
let response = app
.post(&format!("/admin/api/v1/users/{}/api-keys", alice.id))
.add_header(&add_auth_headers(&pm)[0].0, &add_auth_headers(&pm)[0].1)
.add_header(&add_auth_headers(&pm)[1].0, &add_auth_headers(&pm)[1].1)
.json(&json!({"name": "PM For Alice", "purpose": "realtime"}))
.await;
response.assert_status(axum::http::StatusCode::CREATED);
let key: ApiKeyResponse = response.json();
assert_eq!(key.created_by, alice.id, "created_by should be the target user for individual keys");
assert_eq!(key.user_id, alice.id, "user_id should be the target user");
}
}