Skip to content

Commit c2b9f51

Browse files
authored
Add new resource limits (#8)
* Add new resource limits * Review fixes and additional limits * Remove SSO config limit * Address review comments
1 parent aba5059 commit c2b9f51

28 files changed

Lines changed: 750 additions & 12 deletions

src/config/limits.rs

Lines changed: 189 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,37 +25,135 @@ pub struct LimitsConfig {
2525
/// Resource limits for entity counts.
2626
///
2727
/// These limits prevent unbounded growth of resources that could cause
28-
/// performance issues or resource exhaustion.
28+
/// performance issues or resource exhaustion. Set any limit to 0 for unlimited.
29+
///
30+
/// **Enforcement model:** Limits are best-effort. Under concurrent load, the
31+
/// `count → compare → create` pattern may allow a small number of requests
32+
/// to exceed the configured limit. This is acceptable for configuration
33+
/// guardrails; use database-level constraints for strict enforcement.
2934
#[derive(Debug, Clone, Serialize, Deserialize)]
3035
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
3136
#[serde(deny_unknown_fields)]
3237
pub struct ResourceLimits {
33-
/// Maximum RBAC policies per organization.
34-
/// Set to 0 for unlimited. Default: 100 policies per org.
35-
///
36-
/// This limit prevents resource exhaustion from unbounded policy growth.
37-
/// Organizations hitting this limit must delete or disable existing policies
38-
/// before creating new ones.
38+
/// Maximum RBAC policies per organization. Default: 100.
3939
#[serde(default = "default_max_policies_per_org")]
4040
pub max_policies_per_org: u32,
4141

42-
/// Maximum dynamic providers per user (BYOK).
43-
/// Set to 0 for unlimited. Default: 10 providers per user.
42+
/// Maximum dynamic providers per user (BYOK). Default: 10.
4443
#[serde(default = "default_max_providers_per_user")]
4544
pub max_providers_per_user: u32,
4645

47-
/// Maximum API keys per user (self-service).
48-
/// Set to 0 for unlimited. Default: 25 keys per user.
46+
/// Maximum dynamic providers per organization. Default: 100.
47+
#[serde(default = "default_max_providers_per_org")]
48+
pub max_providers_per_org: u32,
49+
50+
/// Maximum dynamic providers per team. Default: 50.
51+
#[serde(default = "default_max_providers_per_team")]
52+
pub max_providers_per_team: u32,
53+
54+
/// Maximum dynamic providers per project. Default: 50.
55+
#[serde(default = "default_max_providers_per_project")]
56+
pub max_providers_per_project: u32,
57+
58+
/// Maximum API keys per user (self-service). Default: 25.
4959
#[serde(default = "default_max_api_keys_per_user")]
5060
pub max_api_keys_per_user: u32,
61+
62+
/// Maximum API keys per organization. Default: 500.
63+
#[serde(default = "default_max_api_keys_per_org")]
64+
pub max_api_keys_per_org: u32,
65+
66+
/// Maximum API keys per team. Default: 100.
67+
#[serde(default = "default_max_api_keys_per_team")]
68+
pub max_api_keys_per_team: u32,
69+
70+
/// Maximum API keys per project. Default: 100.
71+
#[serde(default = "default_max_api_keys_per_project")]
72+
pub max_api_keys_per_project: u32,
73+
74+
/// Maximum teams per organization. Default: 100.
75+
#[serde(default = "default_max_teams_per_org")]
76+
pub max_teams_per_org: u32,
77+
78+
/// Maximum projects per organization. Default: 1000.
79+
#[serde(default = "default_max_projects_per_org")]
80+
pub max_projects_per_org: u32,
81+
82+
/// Maximum service accounts per organization. Default: 50.
83+
#[serde(default = "default_max_service_accounts_per_org")]
84+
pub max_service_accounts_per_org: u32,
85+
86+
/// Maximum vector stores per owner (org/team/project/user). Default: 100.
87+
#[serde(default = "default_max_vector_stores_per_owner")]
88+
pub max_vector_stores_per_owner: u32,
89+
90+
/// Maximum files per vector store. Default: 10,000.
91+
#[serde(default = "default_max_files_per_vector_store")]
92+
pub max_files_per_vector_store: u32,
93+
94+
/// Maximum conversations per owner (project/user). Default: 10,000.
95+
#[serde(default = "default_max_conversations_per_owner")]
96+
pub max_conversations_per_owner: u32,
97+
98+
/// Maximum prompts per owner (org/team/project/user). Default: 5,000.
99+
#[serde(default = "default_max_prompts_per_owner")]
100+
pub max_prompts_per_owner: u32,
101+
102+
/// Maximum domain verifications per SSO configuration. Default: 50.
103+
#[serde(default = "default_max_domains_per_sso_config")]
104+
pub max_domains_per_sso_config: u32,
105+
106+
/// Maximum SSO group mappings per organization. Default: 500.
107+
#[serde(default = "default_max_sso_group_mappings_per_org")]
108+
pub max_sso_group_mappings_per_org: u32,
109+
110+
/// Maximum members per organization. Default: 10,000.
111+
#[serde(default = "default_max_members_per_org")]
112+
pub max_members_per_org: u32,
113+
114+
/// Maximum members per team. Default: 10,000.
115+
#[serde(default = "default_max_members_per_team")]
116+
pub max_members_per_team: u32,
117+
118+
/// Maximum members per project. Default: 10,000.
119+
#[serde(default = "default_max_members_per_project")]
120+
pub max_members_per_project: u32,
121+
122+
/// Maximum uploaded files per owner (org/team/project/user). Default: 10,000.
123+
#[serde(default = "default_max_files_per_owner")]
124+
pub max_files_per_owner: u32,
125+
126+
/// Maximum projects per team. Default: 100.
127+
#[serde(default = "default_max_projects_per_team")]
128+
pub max_projects_per_team: u32,
51129
}
52130

53131
impl Default for ResourceLimits {
54132
fn default() -> Self {
55133
Self {
56134
max_policies_per_org: default_max_policies_per_org(),
57135
max_providers_per_user: default_max_providers_per_user(),
136+
max_providers_per_org: default_max_providers_per_org(),
137+
max_providers_per_team: default_max_providers_per_team(),
138+
max_providers_per_project: default_max_providers_per_project(),
58139
max_api_keys_per_user: default_max_api_keys_per_user(),
140+
max_api_keys_per_org: default_max_api_keys_per_org(),
141+
max_api_keys_per_team: default_max_api_keys_per_team(),
142+
max_api_keys_per_project: default_max_api_keys_per_project(),
143+
max_teams_per_org: default_max_teams_per_org(),
144+
max_projects_per_org: default_max_projects_per_org(),
145+
max_service_accounts_per_org: default_max_service_accounts_per_org(),
146+
max_vector_stores_per_owner: default_max_vector_stores_per_owner(),
147+
max_files_per_vector_store: default_max_files_per_vector_store(),
148+
max_conversations_per_owner: default_max_conversations_per_owner(),
149+
max_prompts_per_owner: default_max_prompts_per_owner(),
150+
max_domains_per_sso_config: default_max_domains_per_sso_config(),
151+
max_sso_group_mappings_per_org: default_max_sso_group_mappings_per_org(),
152+
max_members_per_org: default_max_members_per_org(),
153+
max_members_per_team: default_max_members_per_team(),
154+
max_members_per_project: default_max_members_per_project(),
155+
max_files_per_owner: default_max_files_per_owner(),
156+
max_projects_per_team: default_max_projects_per_team(),
59157
}
60158
}
61159
}
@@ -68,10 +166,90 @@ fn default_max_providers_per_user() -> u32 {
68166
10
69167
}
70168

169+
fn default_max_providers_per_org() -> u32 {
170+
100
171+
}
172+
173+
fn default_max_providers_per_team() -> u32 {
174+
50
175+
}
176+
177+
fn default_max_providers_per_project() -> u32 {
178+
50
179+
}
180+
71181
fn default_max_api_keys_per_user() -> u32 {
72182
25
73183
}
74184

185+
fn default_max_api_keys_per_org() -> u32 {
186+
500
187+
}
188+
189+
fn default_max_api_keys_per_team() -> u32 {
190+
100
191+
}
192+
193+
fn default_max_api_keys_per_project() -> u32 {
194+
100
195+
}
196+
197+
fn default_max_teams_per_org() -> u32 {
198+
100
199+
}
200+
201+
fn default_max_projects_per_org() -> u32 {
202+
1000
203+
}
204+
205+
fn default_max_service_accounts_per_org() -> u32 {
206+
50
207+
}
208+
209+
fn default_max_vector_stores_per_owner() -> u32 {
210+
100
211+
}
212+
213+
fn default_max_files_per_vector_store() -> u32 {
214+
10_000
215+
}
216+
217+
fn default_max_conversations_per_owner() -> u32 {
218+
10_000
219+
}
220+
221+
fn default_max_prompts_per_owner() -> u32 {
222+
5_000
223+
}
224+
225+
fn default_max_domains_per_sso_config() -> u32 {
226+
50
227+
}
228+
229+
fn default_max_sso_group_mappings_per_org() -> u32 {
230+
500
231+
}
232+
233+
fn default_max_members_per_org() -> u32 {
234+
10_000
235+
}
236+
237+
fn default_max_members_per_team() -> u32 {
238+
10_000
239+
}
240+
241+
fn default_max_members_per_project() -> u32 {
242+
10_000
243+
}
244+
245+
fn default_max_files_per_owner() -> u32 {
246+
10_000
247+
}
248+
249+
fn default_max_projects_per_team() -> u32 {
250+
100
251+
}
252+
75253
/// Rate limiting defaults.
76254
#[derive(Debug, Clone, Serialize, Deserialize)]
77255
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]

src/db/postgres/files.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,21 @@ impl FilesRepo for PostgresFilesRepo {
406406
Ok(())
407407
}
408408

409+
async fn count_by_owner(
410+
&self,
411+
owner_type: VectorStoreOwnerType,
412+
owner_id: Uuid,
413+
) -> DbResult<i64> {
414+
let row = sqlx::query(
415+
"SELECT COUNT(*) as count FROM files WHERE owner_type = $1 AND owner_id = $2",
416+
)
417+
.bind(owner_type.as_str())
418+
.bind(owner_id)
419+
.fetch_one(&self.read_pool)
420+
.await?;
421+
Ok(row.get("count"))
422+
}
423+
409424
async fn count_file_references(&self, file_id: Uuid) -> DbResult<i64> {
410425
let result = sqlx::query(
411426
r#"

src/db/postgres/projects.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,20 @@ impl ProjectRepo for PostgresProjectRepo {
281281
Ok(row.get::<i64, _>("count"))
282282
}
283283

284+
async fn count_by_team(&self, team_id: Uuid, include_deleted: bool) -> DbResult<i64> {
285+
let query = if include_deleted {
286+
"SELECT COUNT(*) as count FROM projects WHERE team_id = $1"
287+
} else {
288+
"SELECT COUNT(*) as count FROM projects WHERE team_id = $1 AND deleted_at IS NULL"
289+
};
290+
291+
let row = sqlx::query(query)
292+
.bind(team_id)
293+
.fetch_one(&self.read_pool)
294+
.await?;
295+
Ok(row.get::<i64, _>("count"))
296+
}
297+
284298
async fn update(&self, id: Uuid, input: UpdateProject) -> DbResult<Project> {
285299
let has_name_update = input.name.is_some();
286300
let has_team_update = input.team_id.is_some();

src/db/postgres/vector_stores.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,33 @@ impl VectorStoresRepo for PostgresVectorStoresRepo {
12191219
Ok(result.rows_affected())
12201220
}
12211221

1222+
// ==================== Counts ====================
1223+
1224+
async fn count_by_owner(
1225+
&self,
1226+
owner_type: VectorStoreOwnerType,
1227+
owner_id: Uuid,
1228+
) -> DbResult<i64> {
1229+
let row = sqlx::query(
1230+
"SELECT COUNT(*) as count FROM vector_stores WHERE owner_type = $1 AND owner_id = $2 AND deleted_at IS NULL",
1231+
)
1232+
.bind(owner_type.as_str())
1233+
.bind(owner_id)
1234+
.fetch_one(&self.read_pool)
1235+
.await?;
1236+
Ok(row.get::<i64, _>("count"))
1237+
}
1238+
1239+
async fn count_files_in_vector_store(&self, vector_store_id: Uuid) -> DbResult<i64> {
1240+
let row = sqlx::query(
1241+
"SELECT COUNT(*) as count FROM vector_store_files WHERE vector_store_id = $1 AND deleted_at IS NULL",
1242+
)
1243+
.bind(vector_store_id)
1244+
.fetch_one(&self.read_pool)
1245+
.await?;
1246+
Ok(row.get::<i64, _>("count"))
1247+
}
1248+
12221249
// ==================== Aggregates ====================
12231250
// Note: Chunk operations are handled by the VectorStore trait,
12241251
// as chunks are stored in the vector database (pgvector/Qdrant), not the relational database.

src/db/repos/files.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ pub trait FilesRepo: Send + Sync {
4040
status_details: Option<String>,
4141
) -> DbResult<()>;
4242

43+
/// Count files by owner
44+
async fn count_by_owner(
45+
&self,
46+
owner_type: VectorStoreOwnerType,
47+
owner_id: Uuid,
48+
) -> DbResult<i64>;
49+
4350
/// Count references to a file across collections
4451
/// Used to determine if a file can be deleted
4552
async fn count_file_references(&self, file_id: Uuid) -> DbResult<i64>;

src/db/repos/projects.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub trait ProjectRepo: Send + Sync {
1919
async fn get_by_slug(&self, org_id: Uuid, slug: &str) -> DbResult<Option<Project>>;
2020
async fn list_by_org(&self, org_id: Uuid, params: ListParams) -> DbResult<ListResult<Project>>;
2121
async fn count_by_org(&self, org_id: Uuid, include_deleted: bool) -> DbResult<i64>;
22+
async fn count_by_team(&self, team_id: Uuid, include_deleted: bool) -> DbResult<i64>;
2223
async fn update(&self, id: Uuid, input: UpdateProject) -> DbResult<Project>;
2324
async fn delete(&self, id: Uuid) -> DbResult<()>;
2425
}

src/db/repos/vector_stores.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,18 @@ pub trait VectorStoresRepo: Send + Sync {
175175
/// Used when deleting a file to clean up any soft-deleted references first.
176176
async fn hard_delete_soft_deleted_references(&self, file_id: Uuid) -> DbResult<u64>;
177177

178+
// ==================== Counts ====================
179+
180+
/// Count vector stores by owner (excluding soft-deleted).
181+
async fn count_by_owner(
182+
&self,
183+
owner_type: VectorStoreOwnerType,
184+
owner_id: Uuid,
185+
) -> DbResult<i64>;
186+
187+
/// Count active (non-deleted) files in a vector store.
188+
async fn count_files_in_vector_store(&self, vector_store_id: Uuid) -> DbResult<i64>;
189+
178190
// ==================== Aggregates ====================
179191
// Note: Chunk operations (create, get, delete) are handled by the VectorStore trait,
180192
// as chunks are stored in the vector database (pgvector/Qdrant), not the relational database.

src/db/sqlite/files.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,20 @@ impl FilesRepo for SqliteFilesRepo {
405405
Ok(())
406406
}
407407

408+
async fn count_by_owner(
409+
&self,
410+
owner_type: VectorStoreOwnerType,
411+
owner_id: Uuid,
412+
) -> DbResult<i64> {
413+
let row =
414+
query("SELECT COUNT(*) as count FROM files WHERE owner_type = ? AND owner_id = ?")
415+
.bind(owner_type.as_str())
416+
.bind(owner_id.to_string())
417+
.fetch_one(&self.pool)
418+
.await?;
419+
Ok(row.col("count"))
420+
}
421+
408422
async fn count_file_references(&self, file_id: Uuid) -> DbResult<i64> {
409423
let result = query(
410424
r#"

src/db/sqlite/projects.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,20 @@ impl ProjectRepo for SqliteProjectRepo {
302302
Ok(row.col::<i64>("count"))
303303
}
304304

305+
async fn count_by_team(&self, team_id: Uuid, include_deleted: bool) -> DbResult<i64> {
306+
let sql = if include_deleted {
307+
"SELECT COUNT(*) as count FROM projects WHERE team_id = ?"
308+
} else {
309+
"SELECT COUNT(*) as count FROM projects WHERE team_id = ? AND deleted_at IS NULL"
310+
};
311+
312+
let row = query(sql)
313+
.bind(team_id.to_string())
314+
.fetch_one(&self.pool)
315+
.await?;
316+
Ok(row.col::<i64>("count"))
317+
}
318+
305319
async fn update(&self, id: Uuid, input: UpdateProject) -> DbResult<Project> {
306320
let has_name_update = input.name.is_some();
307321
let has_team_update = input.team_id.is_some();

0 commit comments

Comments
 (0)