Skip to content

Commit c7a7d52

Browse files
authored
add ingest only keys (#1012)
* add ingest only keys * fix metrics to allow ingestion only keys * fix migration * fix types * loading state on dialog * fix build * close /v1/tag from ingest-only keys * upd message, add claude 4.5
1 parent f5f2dc8 commit c7a7d52

File tree

19 files changed

+4229
-1383
lines changed

19 files changed

+4229
-1383
lines changed

app-server/src/api/v1/metrics.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use actix_web::{post, HttpRequest, HttpResponse};
1+
use actix_web::{HttpRequest, HttpResponse, post};
22

33
use crate::routes::types::ResponseResult;
44

5-
#[post("metrics")]
5+
// /v1/metrics
6+
#[post("")]
67
pub async fn process_metrics(req: HttpRequest) -> ResponseResult {
78
// This is a placeholder that just returns ok, so that client otel exporters
89
// don't fail when trying to send metrics to the server.

app-server/src/api/v1/tag.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ pub enum TagRequest {
3737
WithSpanId(TagRequestWithSpanId),
3838
}
3939

40-
#[post("tag")]
40+
// /v1/tag
41+
#[post("")]
4142
pub async fn tag_trace(
4243
req: Json<TagRequest>,
4344
clickhouse: web::Data<clickhouse::Client>,

app-server/src/api/v1/traces.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ pub struct RabbitMqSpanMessage {
2020
pub events: Vec<Event>,
2121
}
2222

23-
#[post("traces")]
23+
// /v1/traces
24+
#[post("")]
2425
pub async fn process_traces(
2526
req: HttpRequest,
2627
body: Bytes,

app-server/src/auth/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ impl FromRequest for ProjectApiKey {
2626
}
2727
}
2828

29-
pub async fn project_validator(
29+
async fn validate_project_api_key(
3030
req: ServiceRequest,
3131
credentials: BearerAuth,
32+
allow_ingest_only: bool,
3233
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
3334
let config = req
3435
.app_data::<Config>()
@@ -48,6 +49,19 @@ pub async fn project_validator(
4849

4950
match get_api_key_from_raw_value(&db.pool, cache, credentials.token().to_string()).await {
5051
Ok(api_key) => {
52+
// Check if ingest-only keys are allowed for this endpoint
53+
if !allow_ingest_only && api_key.is_ingest_only {
54+
log::warn!(
55+
"Ingest-only API key attempted to access restricted endpoint: project_id={}",
56+
api_key.project_id
57+
);
58+
// Return a blank 404 to match default actix web behavior
59+
let response = actix_web::HttpResponse::NotFound().finish();
60+
return Err((
61+
actix_web::error::InternalError::from_response("", response).into(),
62+
req,
63+
));
64+
}
5165
req.extensions_mut().insert(api_key);
5266
Ok(req)
5367
}
@@ -57,3 +71,19 @@ pub async fn project_validator(
5771
}
5872
}
5973
}
74+
75+
/// Standard project validator - blocks ingest-only keys
76+
pub async fn project_validator(
77+
req: ServiceRequest,
78+
credentials: BearerAuth,
79+
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
80+
validate_project_api_key(req, credentials, false).await
81+
}
82+
83+
/// Ingestion validator - allows ingest-only keys for trace ingestion endpoints
84+
pub async fn project_ingestion_validator(
85+
req: ServiceRequest,
86+
credentials: BearerAuth,
87+
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
88+
validate_project_api_key(req, credentials, true).await
89+
}

app-server/src/db/project_api_keys.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub struct ProjectApiKey {
1010
pub name: Option<String>,
1111
pub hash: String,
1212
pub shorthand: String,
13+
pub is_ingest_only: bool,
1314
}
1415

1516
pub async fn get_api_key(pool: &PgPool, hash: &String) -> Result<ProjectApiKey> {
@@ -19,7 +20,8 @@ pub async fn get_api_key(pool: &PgPool, hash: &String) -> Result<ProjectApiKey>
1920
project_api_keys.project_id,
2021
project_api_keys.name,
2122
project_api_keys.id,
22-
project_api_keys.shorthand
23+
project_api_keys.shorthand,
24+
project_api_keys.is_ingest_only
2325
FROM
2426
project_api_keys
2527
WHERE

app-server/src/main.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,8 @@ fn main() -> anyhow::Result<()> {
745745
log::info!("Spinning up full HTTP server");
746746
HttpServer::new(move || {
747747
let project_auth = HttpAuthentication::bearer(auth::project_validator);
748+
let project_ingestion_auth =
749+
HttpAuthentication::bearer(auth::project_ingestion_validator);
748750

749751
App::new()
750752
.wrap(ErrorHandlers::new().handler(
@@ -768,27 +770,40 @@ fn main() -> anyhow::Result<()> {
768770
.app_data(web::Data::new(connection_for_health.clone()))
769771
.app_data(web::Data::new(query_engine.clone()))
770772
.app_data(web::Data::new(sse_connections_for_http.clone()))
773+
// Ingestion endpoints allow both default and ingest-only keys
771774
.service(
772775
web::scope("/v1/browser-sessions").service(
773776
web::scope("")
774-
.wrap(project_auth.clone())
777+
.wrap(project_ingestion_auth.clone())
775778
.service(api::v1::browser_sessions::create_session_event),
776779
),
777780
)
781+
.service(
782+
web::scope("/v1/traces")
783+
.wrap(project_ingestion_auth.clone())
784+
.service(api::v1::traces::process_traces),
785+
)
786+
.service(
787+
web::scope("/v1/metrics")
788+
.wrap(project_ingestion_auth.clone())
789+
.service(api::v1::metrics::process_metrics),
790+
)
791+
// Default endpoints block ingest-only keys
792+
.service(
793+
web::scope("/v1/tag")
794+
.wrap(project_auth.clone())
795+
.service(api::v1::tag::tag_trace),
796+
)
778797
.service(
779798
web::scope("/v1")
780799
.wrap(project_auth.clone())
781-
.service(api::v1::traces::process_traces)
782800
.service(api::v1::datasets::get_datapoints)
783801
.service(api::v1::datasets::create_datapoints)
784802
.service(api::v1::datasets::get_parquet)
785-
.service(api::v1::metrics::process_metrics)
786-
.service(api::v1::browser_sessions::create_session_event)
787803
.service(api::v1::evals::init_eval)
788804
.service(api::v1::evals::save_eval_datapoints)
789805
.service(api::v1::evals::update_eval_datapoint)
790806
.service(api::v1::evaluators::create_evaluator_score)
791-
.service(api::v1::tag::tag_trace)
792807
.service(api::v1::sql::execute_sql_query)
793808
.service(api::v1::payloads::get_payload),
794809
)

app-server/src/traces/grpc_service.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ impl TraceService for ProcessTracesService {
8282
}
8383
}
8484

85+
/// Authenticates gRPC trace ingestion requests.
86+
/// Note: This endpoint accepts both default and ingest-only API keys,
87+
/// as it's used for writing trace data to the project.
8588
async fn authenticate_request(
8689
metadata: &tonic::metadata::MetadataMap,
8790
pool: &PgPool,

frontend/app/api/projects/[projectId]/api-keys/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type NextRequest } from 'next/server';
2-
import { prettifyError,ZodError } from 'zod/v4';
2+
import { prettifyError, ZodError } from 'zod/v4';
33

44
import { createApiKey, deleteApiKey, getApiKeys } from '@/lib/actions/project-api-keys';
55

@@ -15,6 +15,7 @@ export async function POST(
1515
const result = await createApiKey({
1616
projectId: params.projectId,
1717
name: body.name,
18+
isIngestOnly: body.isIngestOnly,
1819
});
1920

2021
return new Response(JSON.stringify(result), {

frontend/components/playground/types.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,16 @@ export const providers: { provider: Provider; models: LanguageModel[] }[] = [
121121
name: "claude-3-opus-20240229",
122122
label: "Claude 3 Opus",
123123
},
124-
{
125-
id: "anthropic:claude-3-5-sonnet-20241022",
126-
name: "claude-3-5-sonnet-20241022",
127-
label: "Claude 3.5 Sonnet",
128-
},
129124
{
130125
id: "anthropic:claude-3-5-haiku-20241022",
131126
name: "claude-3-5-haiku-20241022",
132127
label: "Claude 3.5 Haiku",
133128
},
129+
{
130+
id: "anthropic:claude-3-5-sonnet-20241022",
131+
name: "claude-3-5-sonnet-20241022",
132+
label: "Claude 3.5 Sonnet",
133+
},
134134
{
135135
id: "anthropic:claude-3-7-sonnet-20250219",
136136
name: "claude-3-7-sonnet-20250219",
@@ -146,6 +146,21 @@ export const providers: { provider: Provider; models: LanguageModel[] }[] = [
146146
name: "claude-opus-4-20250514",
147147
label: "Claude 4 Opus",
148148
},
149+
{
150+
id: "anthropic:claude-opus-4-1-20250805",
151+
name: "claude-opus-4-1-20250805",
152+
label: "Claude 4.1 Opus",
153+
},
154+
{
155+
id: "anthropic:claude-haiku-4-5-20251001",
156+
name: "claude-haiku-4-5-20251001",
157+
label: "Claude 4.5 Haiku",
158+
},
159+
{
160+
id: "anthropic:claude-sonnet-4-5-20250929",
161+
name: "claude-sonnet-4-5-20250929",
162+
label: "Claude 4.5 Sonnet",
163+
},
149164
],
150165
},
151166
{
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { GenerateProjectApiKeyResponse } from "@/lib/api-keys/types";
2+
3+
import { Button } from "../../ui/button";
4+
import { CopyButton } from "../../ui/copy-button";
5+
import { DialogFooter } from "../../ui/dialog";
6+
import { Input } from "../../ui/input";
7+
8+
interface DisplayKeyDialogContentProps {
9+
apiKey: GenerateProjectApiKeyResponse;
10+
onClose?: () => void;
11+
}
12+
13+
export function DisplayKeyDialogContent({ apiKey, onClose }: DisplayKeyDialogContentProps) {
14+
return (
15+
<>
16+
<div className="flex flex-col space-y-2">
17+
<p className="text-secondary-foreground text-sm">
18+
{" "}
19+
For security reasons, you will not be able to see this key again. Make sure to copy and save it somewhere
20+
safe.{" "}
21+
</p>
22+
<div className="flex gap-x-2">
23+
<Input className="flex text-sm" value={apiKey.value} readOnly />
24+
<CopyButton size="icon" className="min-w-8 h-8" text={apiKey.value} />
25+
</div>
26+
</div>
27+
<DialogFooter>
28+
<Button onClick={onClose} handleEnter variant="secondary">
29+
Close
30+
</Button>
31+
</DialogFooter>
32+
</>
33+
);
34+
}

0 commit comments

Comments
 (0)