feat(api-key): add allowed_models/allowed_providers and enforce in routing (#85, #86) - #90
Conversation
Bumps [axum-test](https://github.com/JosephLenton/axum-test) from 15.7.4 to 20.1.0. - [Release notes](https://github.com/JosephLenton/axum-test/releases) - [Commits](https://github.com/JosephLenton/axum-test/commits) --- updated-dependencies: - dependency-name: axum-test dependency-version: 20.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…lowed-models-providers
|
Warning Review limit reached
More reviews will be available in 14 minutes and 38 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (29)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
…nt (#89) * feat(api-key): typed ApiKeyScope with canonical scope values (#83) Add KnownScope enum with five canonical values: chat:read, chat:write, providers:read, providers:write, admin. ApiKeyScope::parse now rejects unknown values with UnknownScope error. ApiKeyScope::parse_lenient is introduced for DB reads — accepts any non-empty value and emits a tracing::warn for unrecognised scopes. ManageApiKeys::create and update validate all scopes before touching the repository. Fixes pre-existing sha2 0.11 compilation breakage in login.rs, validate_session.rs, and auth.rs. * feat(authz): scope enforcement per route class in client API policy (#84) Add required_scope(method, path) mapping routes under /v1/* to their canonical scope requirement. Add check_scope helper that allows requests when the subject holds the required scope or the admin superset scope, and rejects with HTTP 403 INSUFFICIENT_SCOPE otherwise. Thread method and path through evaluate_policy and client_api_policy. Update env-fallback credentials to use canonical scope names. Update all affected tests to use canonical scope values. * fix: apply CodeRabbit auto-fixes Fixed 8 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> * feat(api-key): add allowed_models/allowed_providers and enforce in routing (#85, #86) (#90) * chore(deps-rust)(deps): bump axum-test from 15.7.4 to 20.1.0 (#78) Bumps [axum-test](https://github.com/JosephLenton/axum-test) from 15.7.4 to 20.1.0. - [Release notes](https://github.com/JosephLenton/axum-test/releases) - [Commits](https://github.com/JosephLenton/axum-test/commits) --- updated-dependencies: - dependency-name: axum-test dependency-version: 20.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(api-key): add allowed_models and allowed_providers restriction fields (#85) * feat(routing): enforce allowed_models and allowed_providers restrictions (#86) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(deps): add hex crate version 0.4.3 to Cargo.lock * fix(dashboard): update API key scope options to canonical chat:read/chat:write The dashboard was still emitting pre-#83 scope values ('read', 'write') in the create/edit modals, which the backend now rejects with 400 'unknown API key scope'. This was surfaced by 'just ci-local' failing the Playwright e2e suite after #83/#84 landed. Without this fix, every dashboard user creating an API key via the UI would have hit the same 400 error. - ApiKeysView.vue: dropdown values updated to canonical scopes - api-keys.spec.ts: helper defaults + 2 call sites updated; UI selector tightened to '^chat read$' to avoid accidental matches * fix(authz): address code review findings (#1-#4) Four pre-existing issues caught by inline review of the PR-A stack. Finding 5 (SESSION_NOT_FOUND redirect) was verified invalid and skipped — the dashboard redirect is driven by currentUser, not by rejection codes. ## Finding 1: stream paths bypassed forbidden/rate-limit HTTP mapping chat_completions_stream and anthropic_messages_stream were returning SSE 200 with a generic internal_error event when the upstream execute_stream_with_format returned a forbidden or rate_limited error. This means a model-restricted key streaming chat completions got 200 + a confusing SSE error event instead of a clean HTTP 403. Added Err-arms for is_forbidden() and is_rate_limited() in both stream handlers so streaming and non-streaming requests share identical auth/rate-limit behavior. New helpers map_forbidden_openai and map_rate_limited return typed HttpError for the IntoResponse path. ## Finding 2: restrictions_from_headers failed open on missing headers The function used unwrap_or_default() on header lookups, so a missing x-authz-allowed-models or x-authz-allowed-providers header was silently treated as 'unrestricted'. The authz middleware must always stamp these headers, so a missing header indicates either a routing bug or a middleware bypass — both should be loud, not silent. Restructured into a parse_csv_header helper that returns Result<Vec<String>, HttpError> and propagates AUTHZ_HEADER_MISSING or AUTHZ_HEADER_INVALID 500 responses. Empty header value (public subject) still maps to empty Vec, which the domain treats as unrestricted. ## Finding 3: scopes_from_json rejected pre-#83 legacy scope strings auth-sqlite used ApiKeyScope::parse (strict) in scopes_from_json, which rejects any unknown scope string. Existing API keys created before #83 with legacy values ('read', 'write') would fail to load. Switched to ApiKeyScope::parse_lenient, which is the documented method for reading from the database (accepts unknowns, logs warning). Added regression test read_key_with_legacy_scope_string_is_preserved. ## Finding 4: required_scope fallback allowed POST with read-only key required_scope returned Some("chat:read") for ANY /v1/* path that wasn't /v1/providers/* or /v1/chat/* — regardless of HTTP method. This meant a key with only the chat:read scope could hit POST /v1/messages (Anthropic) and pass the authz check, then rely on downstream luck. Updated the fallback to inspect the method: GET → chat:read, all others → chat:write. The special-cases for /v1/providers* and /v1/chat/* are preserved. Added regression test client_api_with_chat_read_scope_rejected_on_post_to_messages. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
Implements two issues from Phase 2: Auth & Provider Management:
allowed_modelsandallowed_providersrestriction fields onApiKeyRecordChanges
Data layer (#85)
ApiKeyRecordgainsallowed_models: Vec<ModelId>andallowed_providers: Vec<ProviderId>(empty = unrestricted)V1__allowed_models_providers.sqladds two TEXT columns toapi_keys(default'[]')CreateApiKeyRequest/UpdateApiKeyRequestaccept these fieldsPOST /api/api-keys,PUT /api/api-keys/:id,GET /api/api-keys,GET /api/api-keys/:idpass them throughEnforcement layer (#86)
CortexError::forbidden()+forbidden_code()distinguishmodel_not_allowedvsprovider_not_allowedRouteRequestchecks model restriction before any provider interaction, and provider restriction afterrouter.select()execute_streamcode; Anthropic handler returns plain text matching its existing style)x-authz-allowed-models/x-authz-allowed-providersheaders fromApiKeySubjectso handlers can hydrateCompletionRequest.restrictionsTesting
route_request.rscovering model/provider restrictions for bothexecuteandexecute_streamauth-sqlitefor create/update with restrictionsclippy --workspace --all-targets -- -D warningscleanArchitecture
ApiKeyRecord(domain) → DTOs (transport) → wire format viaApiKeySubjectRouteRequest) so it applies regardless of which handler invokes it (OpenAI, Anthropic, etc.)CompletionRequestalready hadrestrictionsfield with#[serde(default)]so internal callers are unaffected)Notes
feat/api-key-scope-enforcement(PR feat(api-key): typed scopes with canonical values and route enforcement #89) — will rebase once that merges'[]')Closes #85, closes #86