Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 198 additions & 11 deletions src/config/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,140 @@ pub struct LimitsConfig {
/// Resource limits for entity counts.
///
/// These limits prevent unbounded growth of resources that could cause
/// performance issues or resource exhaustion.
/// performance issues or resource exhaustion. Set any limit to 0 for unlimited.
///
/// **Enforcement model:** Limits are best-effort. Under concurrent load, the
/// `count → compare → create` pattern may allow a small number of requests
/// to exceed the configured limit. This is acceptable for configuration
/// guardrails; use database-level constraints for strict enforcement.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ResourceLimits {
/// Maximum RBAC policies per organization.
/// Set to 0 for unlimited. Default: 100 policies per org.
///
/// This limit prevents resource exhaustion from unbounded policy growth.
/// Organizations hitting this limit must delete or disable existing policies
/// before creating new ones.
/// Maximum RBAC policies per organization. Default: 100.
#[serde(default = "default_max_policies_per_org")]
pub max_policies_per_org: u32,

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

/// Maximum API keys per user (self-service).
/// Set to 0 for unlimited. Default: 25 keys per user.
/// Maximum dynamic providers per organization. Default: 100.
#[serde(default = "default_max_providers_per_org")]
pub max_providers_per_org: u32,

/// Maximum dynamic providers per team. Default: 50.
#[serde(default = "default_max_providers_per_team")]
pub max_providers_per_team: u32,

/// Maximum dynamic providers per project. Default: 50.
#[serde(default = "default_max_providers_per_project")]
pub max_providers_per_project: u32,

/// Maximum API keys per user (self-service). Default: 25.
#[serde(default = "default_max_api_keys_per_user")]
pub max_api_keys_per_user: u32,

/// Maximum API keys per organization. Default: 500.
#[serde(default = "default_max_api_keys_per_org")]
pub max_api_keys_per_org: u32,

/// Maximum API keys per team. Default: 100.
#[serde(default = "default_max_api_keys_per_team")]
pub max_api_keys_per_team: u32,

/// Maximum API keys per project. Default: 100.
#[serde(default = "default_max_api_keys_per_project")]
pub max_api_keys_per_project: u32,

/// Maximum teams per organization. Default: 100.
#[serde(default = "default_max_teams_per_org")]
pub max_teams_per_org: u32,

/// Maximum projects per organization. Default: 1000.
#[serde(default = "default_max_projects_per_org")]
pub max_projects_per_org: u32,

/// Maximum service accounts per organization. Default: 50.
#[serde(default = "default_max_service_accounts_per_org")]
pub max_service_accounts_per_org: u32,

/// Maximum vector stores per owner (org/team/project/user). Default: 100.
#[serde(default = "default_max_vector_stores_per_owner")]
pub max_vector_stores_per_owner: u32,

/// Maximum files per vector store. Default: 10,000.
#[serde(default = "default_max_files_per_vector_store")]
pub max_files_per_vector_store: u32,

/// Maximum conversations per owner (project/user). Default: 10,000.
#[serde(default = "default_max_conversations_per_owner")]
pub max_conversations_per_owner: u32,

/// Maximum prompts per owner (org/team/project/user). Default: 5,000.
#[serde(default = "default_max_prompts_per_owner")]
pub max_prompts_per_owner: u32,

/// Maximum SSO configurations per organization. Default: 5.
#[serde(default = "default_max_sso_configs_per_org")]
pub max_sso_configs_per_org: u32,

/// Maximum domain verifications per SSO configuration. Default: 50.
#[serde(default = "default_max_domains_per_sso_config")]
pub max_domains_per_sso_config: u32,

/// Maximum SSO group mappings per organization. Default: 500.
#[serde(default = "default_max_sso_group_mappings_per_org")]
pub max_sso_group_mappings_per_org: u32,

/// Maximum members per organization. Default: 10,000.
#[serde(default = "default_max_members_per_org")]
pub max_members_per_org: u32,

/// Maximum members per team. Default: 10,000.
#[serde(default = "default_max_members_per_team")]
pub max_members_per_team: u32,

/// Maximum members per project. Default: 10,000.
#[serde(default = "default_max_members_per_project")]
pub max_members_per_project: u32,

/// Maximum uploaded files per owner (org/team/project/user). Default: 10,000.
#[serde(default = "default_max_files_per_owner")]
pub max_files_per_owner: u32,

/// Maximum projects per team. Default: 100.
#[serde(default = "default_max_projects_per_team")]
pub max_projects_per_team: u32,
}

impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_policies_per_org: default_max_policies_per_org(),
max_providers_per_user: default_max_providers_per_user(),
max_providers_per_org: default_max_providers_per_org(),
max_providers_per_team: default_max_providers_per_team(),
max_providers_per_project: default_max_providers_per_project(),
max_api_keys_per_user: default_max_api_keys_per_user(),
max_api_keys_per_org: default_max_api_keys_per_org(),
max_api_keys_per_team: default_max_api_keys_per_team(),
max_api_keys_per_project: default_max_api_keys_per_project(),
max_teams_per_org: default_max_teams_per_org(),
max_projects_per_org: default_max_projects_per_org(),
max_service_accounts_per_org: default_max_service_accounts_per_org(),
max_vector_stores_per_owner: default_max_vector_stores_per_owner(),
max_files_per_vector_store: default_max_files_per_vector_store(),
max_conversations_per_owner: default_max_conversations_per_owner(),
max_prompts_per_owner: default_max_prompts_per_owner(),
max_sso_configs_per_org: default_max_sso_configs_per_org(),
max_domains_per_sso_config: default_max_domains_per_sso_config(),
max_sso_group_mappings_per_org: default_max_sso_group_mappings_per_org(),
max_members_per_org: default_max_members_per_org(),
max_members_per_team: default_max_members_per_team(),
max_members_per_project: default_max_members_per_project(),
max_files_per_owner: default_max_files_per_owner(),
max_projects_per_team: default_max_projects_per_team(),
}
}
}
Expand All @@ -68,10 +171,94 @@ fn default_max_providers_per_user() -> u32 {
10
}

fn default_max_providers_per_org() -> u32 {
100
}

fn default_max_providers_per_team() -> u32 {
50
}

fn default_max_providers_per_project() -> u32 {
50
}

fn default_max_api_keys_per_user() -> u32 {
25
}

fn default_max_api_keys_per_org() -> u32 {
500
}

fn default_max_api_keys_per_team() -> u32 {
100
}

fn default_max_api_keys_per_project() -> u32 {
100
}

fn default_max_teams_per_org() -> u32 {
100
}

fn default_max_projects_per_org() -> u32 {
1000
}

fn default_max_service_accounts_per_org() -> u32 {
50
}

fn default_max_vector_stores_per_owner() -> u32 {
100
}

fn default_max_files_per_vector_store() -> u32 {
10_000
}

fn default_max_conversations_per_owner() -> u32 {
10_000
}

fn default_max_prompts_per_owner() -> u32 {
5_000
}

fn default_max_sso_configs_per_org() -> u32 {
5
}

fn default_max_domains_per_sso_config() -> u32 {
50
}

fn default_max_sso_group_mappings_per_org() -> u32 {
500
}

fn default_max_members_per_org() -> u32 {
10_000
}

fn default_max_members_per_team() -> u32 {
10_000
}

fn default_max_members_per_project() -> u32 {
10_000
}

fn default_max_files_per_owner() -> u32 {
10_000
}

fn default_max_projects_per_team() -> u32 {
100
}

/// Rate limiting defaults.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
Expand Down
15 changes: 15 additions & 0 deletions src/db/postgres/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,21 @@ impl FilesRepo for PostgresFilesRepo {
Ok(())
}

async fn count_by_owner(
&self,
owner_type: VectorStoreOwnerType,
owner_id: Uuid,
) -> DbResult<i64> {
let row = sqlx::query(
"SELECT COUNT(*) as count FROM files WHERE owner_type = $1 AND owner_id = $2",
)
.bind(owner_type.as_str())
.bind(owner_id)
.fetch_one(&self.read_pool)
.await?;
Ok(row.get("count"))
}

async fn count_file_references(&self, file_id: Uuid) -> DbResult<i64> {
let result = sqlx::query(
r#"
Expand Down
8 changes: 8 additions & 0 deletions src/db/postgres/org_sso_configs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,4 +522,12 @@ impl OrgSsoConfigRepo for PostgresOrgSsoConfigRepo {
.await?;
Ok(result.0)
}

async fn count_by_org(&self, org_id: Uuid) -> DbResult<i64> {
let row = sqlx::query("SELECT COUNT(*) as count FROM org_sso_configs WHERE org_id = $1")
.bind(org_id)
.fetch_one(&self.read_pool)
.await?;
Ok(row.get::<i64, _>("count"))
}
}
14 changes: 14 additions & 0 deletions src/db/postgres/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,20 @@ impl ProjectRepo for PostgresProjectRepo {
Ok(row.get::<i64, _>("count"))
}

async fn count_by_team(&self, team_id: Uuid, include_deleted: bool) -> DbResult<i64> {
let query = if include_deleted {
"SELECT COUNT(*) as count FROM projects WHERE team_id = $1"
} else {
"SELECT COUNT(*) as count FROM projects WHERE team_id = $1 AND deleted_at IS NULL"
};

let row = sqlx::query(query)
.bind(team_id)
.fetch_one(&self.read_pool)
.await?;
Ok(row.get::<i64, _>("count"))
}

async fn update(&self, id: Uuid, input: UpdateProject) -> DbResult<Project> {
let has_name_update = input.name.is_some();
let has_team_update = input.team_id.is_some();
Expand Down
27 changes: 27 additions & 0 deletions src/db/postgres/vector_stores.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,33 @@ impl VectorStoresRepo for PostgresVectorStoresRepo {
Ok(result.rows_affected())
}

// ==================== Counts ====================

async fn count_by_owner(
&self,
owner_type: VectorStoreOwnerType,
owner_id: Uuid,
) -> DbResult<i64> {
let row = sqlx::query(
"SELECT COUNT(*) as count FROM vector_stores WHERE owner_type = $1 AND owner_id = $2 AND deleted_at IS NULL",
)
.bind(owner_type.as_str())
.bind(owner_id)
.fetch_one(&self.read_pool)
.await?;
Ok(row.get::<i64, _>("count"))
}

async fn count_files_in_vector_store(&self, vector_store_id: Uuid) -> DbResult<i64> {
let row = sqlx::query(
"SELECT COUNT(*) as count FROM vector_store_files WHERE vector_store_id = $1 AND deleted_at IS NULL",
)
.bind(vector_store_id)
.fetch_one(&self.read_pool)
.await?;
Ok(row.get::<i64, _>("count"))
}

// ==================== Aggregates ====================
// Note: Chunk operations are handled by the VectorStore trait,
// as chunks are stored in the vector database (pgvector/Qdrant), not the relational database.
Expand Down
7 changes: 7 additions & 0 deletions src/db/repos/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ pub trait FilesRepo: Send + Sync {
status_details: Option<String>,
) -> DbResult<()>;

/// Count files by owner
async fn count_by_owner(
&self,
owner_type: VectorStoreOwnerType,
owner_id: Uuid,
) -> DbResult<i64>;

/// Count references to a file across collections
/// Used to determine if a file can be deleted
async fn count_file_references(&self, file_id: Uuid) -> DbResult<i64>;
Expand Down
3 changes: 3 additions & 0 deletions src/db/repos/org_sso_configs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,7 @@ pub trait OrgSsoConfigRepo: Send + Sync {
///
/// Used to determine if email discovery should be shown on the login page.
async fn any_enabled(&self) -> DbResult<bool>;

/// Count SSO configurations for an organization.
async fn count_by_org(&self, org_id: Uuid) -> DbResult<i64>;
}
1 change: 1 addition & 0 deletions src/db/repos/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub trait ProjectRepo: Send + Sync {
async fn get_by_slug(&self, org_id: Uuid, slug: &str) -> DbResult<Option<Project>>;
async fn list_by_org(&self, org_id: Uuid, params: ListParams) -> DbResult<ListResult<Project>>;
async fn count_by_org(&self, org_id: Uuid, include_deleted: bool) -> DbResult<i64>;
async fn count_by_team(&self, team_id: Uuid, include_deleted: bool) -> DbResult<i64>;
async fn update(&self, id: Uuid, input: UpdateProject) -> DbResult<Project>;
async fn delete(&self, id: Uuid) -> DbResult<()>;
}
12 changes: 12 additions & 0 deletions src/db/repos/vector_stores.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,18 @@ pub trait VectorStoresRepo: Send + Sync {
/// Used when deleting a file to clean up any soft-deleted references first.
async fn hard_delete_soft_deleted_references(&self, file_id: Uuid) -> DbResult<u64>;

// ==================== Counts ====================

/// Count vector stores by owner (excluding soft-deleted).
async fn count_by_owner(
&self,
owner_type: VectorStoreOwnerType,
owner_id: Uuid,
) -> DbResult<i64>;

/// Count active (non-deleted) files in a vector store.
async fn count_files_in_vector_store(&self, vector_store_id: Uuid) -> DbResult<i64>;

// ==================== Aggregates ====================
// Note: Chunk operations (create, get, delete) are handled by the VectorStore trait,
// as chunks are stored in the vector database (pgvector/Qdrant), not the relational database.
Expand Down
Loading
Loading