Skip to content

Commit 898109a

Browse files
committed
Add API key requirement to Ingest methods
Add API key management methods Update UI
1 parent 2ec4797 commit 898109a

File tree

20 files changed

+773
-296
lines changed

20 files changed

+773
-296
lines changed

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ reqwest = { version = "0.12.12", default-features = false, features = ["json", "
2828
axum = { version = "0.8.1", features = ["macros"] }
2929
axum-extra = { version = "0.10.0", features = ["typed-header"] }
3030
clap = { version = "4.5.23", features = ["derive", "color", "usage", "env"] }
31+
metrics = "0.24.1"
3132
tokio = { version = "1.43", features = ["macros", "rt", "sync", "rt-multi-thread", "signal", "io-util"] }
3233
url = { version = "2.5.4", features = ["serde"] }
33-
utoipa = { version = "5.3.1", features = ["axum_extras", "uuid", "url"] }
34+
utoipa = { version = "5.3.1", features = ["axum_extras", "chrono", "uuid", "url"] }
3435
utoipa-axum = "0.2"
3536
utoipa-swagger-ui = { version = "9", features = ["axum", "vendored"] }
36-
uuid = { version = "1.12.0", features = ["serde", "v7", "fast-rng"] }
37+
uuid = { version = "1.12.0", features = ["serde", "v4", "v7", "fast-rng"] }
3738

3839
[workspace.dependencies.sea-orm-migration]
3940
version = "1.1.4"

notifico-app/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@ async-trait = "0.1.84"
1717
axum = { workspace = true }
1818
axum-extra = { workspace = true }
1919
backoff = { version = "0.4.0", features = ["tokio"] }
20+
chrono = "0.4.39"
2021
clap = { workspace = true }
2122
dotenvy = "0.15.7"
2223
fe2o3-amqp = { version = "0.13.1" }
2324
flume = "0.11.1"
2425
futures = "0.3.31"
2526
jsonwebtoken = "9.3.0"
2627
log = "0.4.22"
28+
metrics = { workspace = true }
29+
moka = "0.12.10"
30+
multimap = "0.10.0"
2731
rust-embed = { version = "8.5.0", features = ["mime-guess"] }
2832
sea-orm = { workspace = true }
2933
serde = { version = "1.0.217", features = ["derive"] }
@@ -38,4 +42,3 @@ utoipa = { workspace = true }
3842
utoipa-axum = { workspace = true }
3943
utoipa-swagger-ui = { workspace = true }
4044
uuid = { workspace = true }
41-
multimap = "0.10.0"

notifico-app/assets/assets/index-DKhqOV6l.js

Lines changed: 275 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

notifico-app/assets/assets/index-DL7kKpS3.js

Lines changed: 0 additions & 275 deletions
This file was deleted.

notifico-app/assets/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<title>
2323
Refine - Build your React-based CRUD applications, without constraints.
2424
</title>
25-
<script type="module" crossorigin src="/assets/index-DL7kKpS3.js"></script>
25+
<script type="module" crossorigin src="/assets/index-DKhqOV6l.js"></script>
2626
<link rel="modulepreload" crossorigin href="/assets/monaco_editor-RvQpGkV2.js">
2727
<link rel="modulepreload" crossorigin href="/assets/mui-BkgdmF8m.js">
2828
<link rel="stylesheet" crossorigin href="/assets/monaco_editor-DyX1CsEw.css">

notifico-app/migration/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod m20250108_000001_create_project_table;
55
mod m20250108_000002_create_pipeline_event;
66
mod m20250108_000003_recipient;
77
mod m20250121_000001_create_template_table;
8+
mod m20250203_000001_create_apikey_table;
89

910
pub struct Migrator;
1011

@@ -16,6 +17,7 @@ impl MigratorTrait for Migrator {
1617
Box::new(m20250108_000002_create_pipeline_event::Migration),
1718
Box::new(m20250108_000003_recipient::Migration),
1819
Box::new(m20250121_000001_create_template_table::Migration),
20+
Box::new(m20250203_000001_create_apikey_table::Migration),
1921
]
2022
}
2123

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use sea_orm_migration::{prelude::*, schema::*};
2+
3+
#[derive(DeriveMigrationName)]
4+
pub struct Migration;
5+
6+
#[async_trait::async_trait]
7+
impl MigrationTrait for Migration {
8+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9+
manager
10+
.create_table(
11+
Table::create()
12+
.table(ApiKey::Table)
13+
.if_not_exists()
14+
.col(pk_uuid(ApiKey::Id))
15+
.col(uuid_uniq(ApiKey::Key))
16+
.col(uuid(ApiKey::ProjectId))
17+
.col(string(ApiKey::Description).default(""))
18+
.col(date_time(ApiKey::CreatedAt).default(Expr::current_timestamp()))
19+
.foreign_key(
20+
ForeignKey::create()
21+
.from(ApiKey::Table, ApiKey::ProjectId)
22+
.to(Project::Table, Project::Id)
23+
.on_delete(ForeignKeyAction::Cascade)
24+
.on_update(ForeignKeyAction::Restrict),
25+
)
26+
.to_owned(),
27+
)
28+
.await
29+
}
30+
31+
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
32+
unimplemented!()
33+
}
34+
}
35+
36+
#[derive(DeriveIden)]
37+
enum ApiKey {
38+
Table,
39+
Id,
40+
Key,
41+
ProjectId,
42+
Description,
43+
CreatedAt,
44+
}
45+
46+
#[derive(DeriveIden)]
47+
enum Project {
48+
Table,
49+
Id,
50+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
use crate::crud_table::{
2+
AdminCrudError, AdminCrudTable, ItemWithId, ListQueryParams, ListableTrait, PaginatedResult,
3+
};
4+
use crate::entity;
5+
use futures::FutureExt;
6+
use metrics::{counter, gauge, Counter, Gauge};
7+
use moka::future::Cache;
8+
use moka::notification::ListenerFuture;
9+
use sea_orm::ActiveValue::Unchanged;
10+
use sea_orm::{
11+
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, NotSet, PaginatorTrait,
12+
QueryFilter, Set,
13+
};
14+
use serde::{Deserialize, Serialize};
15+
use std::time::Duration;
16+
use utoipa::ToSchema;
17+
use uuid::Uuid;
18+
19+
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
20+
pub struct ApiKey {
21+
pub key: Uuid,
22+
pub description: String,
23+
pub project_id: Uuid,
24+
pub created_at: Option<chrono::NaiveDateTime>,
25+
}
26+
27+
pub struct ApiKeyController {
28+
db: DatabaseConnection,
29+
authorization_cache: Cache<Uuid, Uuid>,
30+
authorization_cache_gauge: Gauge,
31+
authorization_cache_hit: Counter,
32+
authorization_cache_miss: Counter,
33+
authorization_invalid_key: Counter,
34+
}
35+
36+
impl ApiKeyController {
37+
pub fn new(db: DatabaseConnection) -> Self {
38+
let authorization_cache_capacity = 100;
39+
gauge!("ingest_api_key_cache_capacity").set(authorization_cache_capacity as f64);
40+
41+
let authorization_cache_gauge = gauge!("ingest_api_key_cache_total");
42+
let authorization_cache_gauge_for_fut = authorization_cache_gauge.clone();
43+
44+
let authorization_cache = Cache::builder()
45+
.max_capacity(authorization_cache_capacity)
46+
.time_to_live(Duration::from_secs(1))
47+
.async_eviction_listener(move |_, _, _| -> ListenerFuture {
48+
authorization_cache_gauge_for_fut.decrement(1);
49+
async {}.boxed()
50+
})
51+
.build();
52+
53+
Self {
54+
db,
55+
authorization_cache,
56+
authorization_cache_gauge,
57+
authorization_cache_hit: counter!("ingest_api_key_cache_hit"),
58+
authorization_cache_miss: counter!("ingest_api_key_cache_miss"),
59+
authorization_invalid_key: counter!("ingest_api_key_invalid"),
60+
}
61+
}
62+
}
63+
64+
impl From<entity::api_key::Model> for ApiKey {
65+
fn from(value: entity::api_key::Model) -> Self {
66+
ApiKey {
67+
key: value.key,
68+
description: value.description,
69+
project_id: value.project_id,
70+
created_at: Some(value.created_at),
71+
}
72+
}
73+
}
74+
75+
impl AdminCrudTable for ApiKeyController {
76+
type Item = ApiKey;
77+
78+
async fn get_by_id(&self, id: Uuid) -> Result<Option<Self::Item>, AdminCrudError> {
79+
let query = entity::api_key::Entity::find_by_id(id)
80+
.one(&self.db)
81+
.await?;
82+
Ok(query.map(ApiKey::from))
83+
}
84+
85+
async fn list(
86+
&self,
87+
params: ListQueryParams,
88+
) -> Result<PaginatedResult<ItemWithId<Self::Item>>, AdminCrudError> {
89+
let params = params.try_into()?;
90+
let query = entity::api_key::Entity::find()
91+
.apply_params(&params)
92+
.unwrap()
93+
.all(&self.db)
94+
.await?;
95+
96+
Ok(PaginatedResult {
97+
items: query
98+
.into_iter()
99+
.map(|m| ItemWithId {
100+
id: m.id,
101+
item: ApiKey::from(m),
102+
})
103+
.collect(),
104+
total: entity::api_key::Entity::find()
105+
.apply_filter(&params)?
106+
.count(&self.db)
107+
.await?,
108+
})
109+
}
110+
111+
async fn create(&self, item: Self::Item) -> Result<ItemWithId<Self::Item>, AdminCrudError> {
112+
let id = Uuid::now_v7();
113+
let key = Uuid::new_v4();
114+
115+
entity::api_key::ActiveModel {
116+
id: Set(id),
117+
key: Set(key),
118+
project_id: Set(item.project_id),
119+
description: Set(item.description.to_string()),
120+
created_at: NotSet,
121+
}
122+
.insert(&self.db)
123+
.await?;
124+
125+
Ok(ItemWithId {
126+
id,
127+
item: ApiKey {
128+
key,
129+
description: item.description.to_string(),
130+
project_id: item.project_id,
131+
created_at: Some(chrono::Utc::now().naive_utc()),
132+
},
133+
})
134+
}
135+
136+
async fn update(
137+
&self,
138+
id: Uuid,
139+
item: Self::Item,
140+
) -> Result<ItemWithId<Self::Item>, AdminCrudError> {
141+
entity::api_key::ActiveModel {
142+
id: Unchanged(id),
143+
key: NotSet,
144+
project_id: NotSet,
145+
description: Set(item.description.to_string()),
146+
created_at: NotSet,
147+
}
148+
.update(&self.db)
149+
.await?;
150+
Ok(ItemWithId { id, item })
151+
}
152+
153+
async fn delete(&self, id: Uuid) -> Result<(), AdminCrudError> {
154+
entity::api_key::Entity::delete_by_id(id)
155+
.exec(&self.db)
156+
.await?;
157+
Ok(())
158+
}
159+
}
160+
161+
pub enum ApiKeyError {
162+
InvalidApiKey,
163+
InternalError,
164+
}
165+
166+
impl ApiKeyController {
167+
pub async fn authorize_api_key(&self, key: &str) -> Result<Uuid, ApiKeyError> {
168+
let Ok(key_uuid) = Uuid::try_parse(key) else {
169+
self.authorization_invalid_key.increment(1);
170+
return Err(ApiKeyError::InvalidApiKey);
171+
};
172+
173+
let cached_project_id = self.authorization_cache.get(&key_uuid).await;
174+
175+
if let Some(project_id) = cached_project_id {
176+
// Cache Hit
177+
self.authorization_cache_hit.increment(1);
178+
Ok(project_id)
179+
} else {
180+
// Cache Miss
181+
self.authorization_cache_miss.increment(1);
182+
183+
let Some(api_key_entry) = entity::api_key::Entity::find()
184+
.filter(entity::api_key::Column::Key.eq(key_uuid))
185+
.one(&self.db)
186+
.await
187+
.map_err(|_| ApiKeyError::InternalError)?
188+
else {
189+
self.authorization_invalid_key.increment(1);
190+
return Err(ApiKeyError::InvalidApiKey);
191+
};
192+
193+
let project_id = api_key_entry.project_id;
194+
195+
self.authorization_cache.insert(key_uuid, project_id).await;
196+
self.authorization_cache_gauge.increment(1);
197+
198+
Ok(project_id)
199+
}
200+
}
201+
}

notifico-app/src/controllers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod api_key;
12
pub mod event;
23
pub mod group;
34
pub mod pipeline;

0 commit comments

Comments
 (0)