Skip to content

Add new resource limits#8

Open
ScriptSmith wants to merge 1 commit intomainfrom
limits
Open

Add new resource limits#8
ScriptSmith wants to merge 1 commit intomainfrom
limits

Conversation

@ScriptSmith
Copy link
Owner

No description provided.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 5, 2026

Greptile Summary

This PR systematically adds configurable resource limits across the platform by introducing 15 new fields in ResourceLimits and wiring a count-then-check guard into every relevant create route (API keys, dynamic providers, teams, projects, service accounts, conversations, prompts, vector stores, SSO configs, domain verifications, and SSO group mappings). The pattern is consistent throughout: read max from config, skip if 0 (unlimited), otherwise count active records via a new DB method and reject with 409 Conflict if the limit is reached.

Key findings:

  • Logic bug in src/routes/admin/org_sso_configs.rs: When max_sso_configs_per_org = 0 (unlimited), an else if fallback still enforces the legacy one-per-org application-level check, contradicting the ResourceLimits doc comment that states "Set any limit to 0 for unlimited." The else if branch should be removed to make the behaviour consistent with the documented contract.

  • TOCTOU race conditions: Every limit check follows a count → compare → create pattern without transactional isolation. Under concurrent load, limits can be exceeded. The code should document whether limits are meant to be best-effort (accepting occasional over-limit scenarios) or strict (requiring transactional enforcement via SELECT … FOR UPDATE or serializable transactions).

The majority of the changes are mechanical and correct — adding config fields and count guards consistently across routes. However, the SSO config logic bug creates a semantic inconsistency that will silently override admin intent when max_sso_configs_per_org is set to 0.

Confidence Score: 3/5

  • Safe to merge with low risk for most resources, but the SSO config logic bug must be resolved before shipping to prevent silent override of admin intent.
  • The majority of the changes are mechanical and correct — adding config fields and count guards consistently across routes. Two issues lower confidence: (1) the else if in org_sso_configs.rs creates a semantic inconsistency that will silently override admin intent when max_sso_configs_per_org is set to 0, and (2) all limit checks share an unacknowledged TOCTOU window that could allow limit bypass under concurrency. However, TOCTOU races may be acceptable as best-effort limits depending on requirements.
  • src/routes/admin/org_sso_configs.rs requires resolution of the else if fallback bug before shipping. Consider documenting TOCTOU behavior (best-effort vs. strict) across all limit-checked routes.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as Route Handler
    participant Config as ResourceLimits (config)
    participant Service
    participant DB as Database (read pool)

    Client->>Route: POST /resource (create request)
    Route->>Route: Authenticate & authorize
    Route->>Config: Read max_<resource>_per_<scope>
    alt max == 0 (unlimited)
        Route->>Route: Skip limit check (proceed)
        Note over Route: ⚠️ Exception: SSO configs fall into<br/>else-if and still enforce 1-per-org
    else max > 0
        Route->>Service: count_by_<scope>(id)
        Service->>DB: SELECT COUNT(*) … WHERE … AND deleted_at IS NULL
        DB-->>Service: current_count
        Service-->>Route: current_count
        alt current_count >= max
            Route-->>Client: 409 Conflict (limit reached)
        else current_count < max
            Note over Route,DB: ⚠️ TOCTOU window — concurrent requests<br/>can both pass this check
            Route->>Service: create(input)
            Service->>DB: INSERT …
            DB-->>Service: new record
            Service-->>Route: created resource
            Route-->>Client: 201 Created
        end
    end
Loading

Comments Outside Diff (1)

  1. General comment

    The else if fallback contradicts the documented "0 = unlimited" semantics. When max_sso_configs_per_org is set to 0, the code skips the configurable limit check but then enforces the legacy one-per-org restriction via get_by_org_id, silently ignoring the admin's intent.

    To fix, remove the else if block entirely so that setting the limit to 0 truly disables the restriction:

    Alternatively, if 0 is deliberately meant to preserve the legacy "at most 1 per org" behavior (different from unlimited), update the struct doc comment in src/config/limits.rs to document this special case, and update the in-code comment to clarify the layering of enforcement (database UNIQUE constraint + application-level fallback).

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/routes/admin/org_sso_configs.rs
    Line: 294-304
    
    Comment:
    The `else if` fallback contradicts the documented "0 = unlimited" semantics. When `max_sso_configs_per_org` is set to `0`, the code skips the configurable limit check but then enforces the legacy one-per-org restriction via `get_by_org_id`, silently ignoring the admin's intent.
    
    To fix, remove the `else if` block entirely so that setting the limit to `0` truly disables the restriction:
    
    
    
    Alternatively, if `0` is deliberately meant to preserve the legacy "at most 1 per org" behavior (different from unlimited), update the struct doc comment in `src/config/limits.rs` to document this special case, and update the in-code comment to clarify the layering of enforcement (database UNIQUE constraint + application-level fallback).
    
    How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 556bab3

Comment on lines +322 to +373
// Check per-scope API key limits
let limits = &state.config.limits.resource_limits;
match &input.owner {
crate::models::ApiKeyOwner::Organization { org_id } => {
let max = limits.max_api_keys_per_org;
if max > 0 {
let count = services.api_keys.count_by_org(*org_id, false).await?;
if count >= max as i64 {
return Err(AdminError::Conflict(format!(
"Organization has reached the maximum number of API keys ({max})"
)));
}
}
}
crate::models::ApiKeyOwner::Team { team_id } => {
let max = limits.max_api_keys_per_team;
if max > 0 {
let count = services.api_keys.count_by_team(*team_id, false).await?;
if count >= max as i64 {
return Err(AdminError::Conflict(format!(
"Team has reached the maximum number of API keys ({max})"
)));
}
}
}
crate::models::ApiKeyOwner::Project { project_id } => {
let max = limits.max_api_keys_per_project;
if max > 0 {
let count = services
.api_keys
.count_by_project(*project_id, false)
.await?;
if count >= max as i64 {
return Err(AdminError::Conflict(format!(
"Project has reached the maximum number of API keys ({max})"
)));
}
}
}
crate::models::ApiKeyOwner::User { user_id } => {
let max = limits.max_api_keys_per_user;
if max > 0 {
let count = services.api_keys.count_by_user(*user_id, false).await?;
if count >= max as i64 {
return Err(AdminError::Conflict(format!(
"User has reached the maximum number of API keys ({max})"
)));
}
}
}
crate::models::ApiKeyOwner::ServiceAccount { .. } => {}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All limit checks throughout this PR follow a count() → compare() → create() pattern without transactional isolation. Under concurrent load, two or more requests can both read the same count (e.g., max - 1), both pass the limit check, and both proceed to create, resulting in the final count exceeding the configured limit.

This TOCTOU (time-of-check / time-of-use) race affects every limit-checked route in this PR: teams, projects, service_accounts, conversations, prompts, vector_stores, API keys, dynamic providers, SSO configs, domain verifications, and SSO group mappings.

To clarify intent, add a comment near the limit-checking code (here and in other routes) documenting whether limits are:

  • Best-effort (allow occasional over-limit when concurrent requests race), or
  • Strict (require SELECT … FOR UPDATE or serializable transactions to prevent races).

If best-effort is acceptable, document it. If strict enforcement is required, consider wrapping count + insert in a serializable transaction or advisory lock per owner.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/admin/api_keys.rs
Line: 322-373

Comment:
All limit checks throughout this PR follow a `count() → compare() → create()` pattern without transactional isolation. Under concurrent load, two or more requests can both read the same count (e.g., `max - 1`), both pass the limit check, and both proceed to create, resulting in the final count exceeding the configured limit.

This TOCTOU (time-of-check / time-of-use) race affects every limit-checked route in this PR: teams, projects, service_accounts, conversations, prompts, vector_stores, API keys, dynamic providers, SSO configs, domain verifications, and SSO group mappings.

To clarify intent, add a comment near the limit-checking code (here and in other routes) documenting whether limits are:
- **Best-effort** (allow occasional over-limit when concurrent requests race), or  
- **Strict** (require `SELECT … FOR UPDATE` or serializable transactions to prevent races).

If best-effort is acceptable, document it. If strict enforcement is required, consider wrapping count + insert in a serializable transaction or advisory lock per owner.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant