UN-2407 [FEAT] Add global API deployment keys for grouped API deployments#1881
Conversation
…ents Add ability to generate a common API key that works across a group of API deployments, allowing users to manage shared access without creating individual keys per deployment.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds global API deployment keys end to end: backend auth now distinguishes global keys, the API exposes CRUD and rotation for them, and the frontend adds an admin settings page to manage keys. Safe-text validation is moved into a shared utility. ChangesGlobal API deployment key feature
Estimated code review effort: 4 (Complex) | ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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 |
|
|
| Filename | Overview |
|---|---|
| backend/global_api_deployment_key/serializers.py | The organization-scoped deployment field resolves the earlier cross-organization assignment issue, and partial updates now leave untouched scopes editable and deactivatable. |
| backend/api_v2/serializers.py | Global-key execution now validates profile organization through its Prompt Studio tool and intentionally fails closed when no organization can be derived. |
| backend/api_v2/deployment_helper.py | Deployment authentication now falls back from a deployment-specific key to a global key and carries the resolved authorization identity downstream. |
| frontend/src/routes/useMainAppRoutes.js | The global API deployment key settings page is registered within the existing administrator-only route boundary. |
| frontend/src/components/settings/global-api-deployment-keys/GlobalApiDeploymentKeys.jsx | Adds management workflows for creating, editing, rotating, deleting, activating, and copying global deployment keys. |
Reviews (11): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (5)
backend/global_api_deployment_key/permissions.py (1)
1-3: Use a feature-specific permission class message.Current re-export will return the upstream message mentioning “platform API keys,” which is misleading for this endpoint.
♻️ Suggested adjustment
-from platform_api.permissions import IsOrganizationAdmin +from platform_api.permissions import IsOrganizationAdmin as PlatformIsOrganizationAdmin + + +class IsOrganizationAdmin(PlatformIsOrganizationAdmin): + message = "Only organization admins can manage global API deployment keys." __all__ = ["IsOrganizationAdmin"]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/global_api_deployment_key/permissions.py` around lines 1 - 3, The re-export of IsOrganizationAdmin exposes the upstream error message mentioning “platform API keys”; create a small feature-specific subclass (e.g., GlobalAPIDeploymentKeyIsOrganizationAdmin) that inherits from IsOrganizationAdmin and overrides the permission error message/attribute to mention global API deployment keys (or this endpoint) and then export that subclass in __all__ instead of the upstream name; update any references to use the new subclass name so the endpoint shows the correct permission message.backend/api_v2/key_helper.py (1)
96-97: Add explicit exception chaining inexceptblocks raisingUnauthorizedKey.Use
raise UnauthorizedKey() from None(orfrom err) to make exception intent explicit and satisfy Ruff B904. This applies to lines 39, 97, and 104.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/api_v2/key_helper.py` around lines 96 - 97, Update the except blocks that currently do "except (ValueError, AttributeError): raise UnauthorizedKey()" to use explicit exception chaining; replace the raise with "raise UnauthorizedKey() from None" (or "from err" if you capture the exception as "except (ValueError, AttributeError) as err:") for each place that raises UnauthorizedKey (the three except blocks that currently raise UnauthorizedKey).frontend/src/components/settings/global-api-deployment-keys/GlobalApiDeploymentKeys.jsx (2)
352-382: MoveDeploymentScopeFieldsoutside the parent component.Defining
DeploymentScopeFieldsinsideGlobalApiDeploymentKeyscauses it to be re-created on every render, which can lead to:
- Loss of component state and focus when parent re-renders
- Unnecessary re-mounts affecting form validation state
Since
DeploymentScopeFieldsusesdeploymentsfrom the parent scope, pass it as a prop.♻️ Proposed refactor
Define
DeploymentScopeFieldsoutside the component (above or in a separate file):// Outside GlobalApiDeploymentKeys function function DeploymentScopeFields({ form, deployments }) { const allowAll = Form.useWatch("allow_all_deployments", form); return ( <> <Form.Item name="allow_all_deployments" valuePropName="checked" initialValue={true} > <Checkbox>Allow all API deployments</Checkbox> </Form.Item> <Form.Item name="api_deployments" label="Select API Deployments"> <Select mode="multiple" placeholder="Search and select deployments" optionFilterProp="children" className="gadk__deployment-select" showSearch disabled={allowAll !== false} > {deployments?.map((d) => ( <Select.Option key={d?.id} value={d?.id}> {d?.display_name || d?.api_name} {!d?.is_active && " (inactive)"} </Select.Option> ))} </Select> </Form.Item> </> ); }Then update usages:
- <DeploymentScopeFields form={createForm} /> + <DeploymentScopeFields form={createForm} deployments={deployments} />- <DeploymentScopeFields form={editForm} /> + <DeploymentScopeFields form={editForm} deployments={deployments} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/settings/global-api-deployment-keys/GlobalApiDeploymentKeys.jsx` around lines 352 - 382, DeploymentScopeFields is currently defined inside GlobalApiDeploymentKeys causing it to be recreated on every render; move the DeploymentScopeFields function out of the GlobalApiDeploymentKeys component (define it above or in a separate file) and accept deployments as a prop (i.e., change signature to DeploymentScopeFields({ form, deployments })) so it uses Form.useWatch("allow_all_deployments", form) but no longer closes over parent scope; update all usages inside GlobalApiDeploymentKeys to pass the form and deployments props accordingly to preserve state, focus and avoid unnecessary remounts.
161-175: Consider disabling the switch during status update to prevent double-clicks.
handleToggleStatusdoesn't prevent concurrent updates. Rapid toggling could cause race conditions or confusing UI states.♻️ Optional: Track loading state per row
const [updatingIds, setUpdatingIds] = useState(new Set()); const handleToggleStatus = (record) => { if (updatingIds.has(record?.id)) return; setUpdatingIds((prev) => new Set(prev).add(record?.id)); axiosPrivate({ // ... existing code }) .then(() => fetchKeys()) .catch((err) => setAlertDetails(handleException(err, "Failed to update key status")) ) .finally(() => { setUpdatingIds((prev) => { const next = new Set(prev); next.delete(record?.id); return next; }); }); }; // In render: <Switch size="small" checked={record?.is_active} loading={updatingIds.has(record?.id)} onChange={() => handleToggleStatus(record)} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/settings/global-api-deployment-keys/GlobalApiDeploymentKeys.jsx` around lines 161 - 175, handleToggleStatus currently allows rapid concurrent requests which can cause race conditions; add a per-row loading guard using a state like updatingIds (useState(new Set())) to ignore toggles when the id is present, set the id into updatingIds before calling axiosPrivate and remove it in finally, and pass updatingIds.has(record?.id) to the Switch component's loading prop while keeping onChange tied to handleToggleStatus so the switch is disabled/indicates loading during the update; ensure you still call fetchKeys() on success and handleException on error.backend/global_api_deployment_key/views.py (1)
75-81: Consider pagination for thedeploymentsendpoint.For organizations with many API deployments, returning all records without pagination could impact performance. This is acceptable for admin-only usage with small datasets, but may need pagination if deployment counts grow.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/global_api_deployment_key/views.py` around lines 75 - 81, The deployments action currently returns all APIDeployment objects unpaginated; update the deployments method to use DRF pagination by calling self.paginate_queryset(APIDeployment.objects.filter(organization=organization)) and, if a page is returned, serialize that page with ApiDeploymentMinimalSerializer and return self.get_paginated_response(serializer.data); otherwise serialize the full queryset and return Response(serializer.data). Keep references to the existing method name deployments, model APIDeployment and serializer ApiDeploymentMinimalSerializer so the change is localized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/api_v2/serializers.py`:
- Around line 395-397: The code currently returns value when is_global_key is
true without checking the profile's org; instead, fetch the profile referenced
by llm_profile_id and compare its organization/tenant to the caller's
organization (obtainable from the serializer context request or passed tenant
id), and if they don't match raise a generic validation error; keep the
early-return for valid matches but replace the unconditional return in the
is_global_key branch with an org equality check that raises a
serializers.ValidationError (or the existing error type) on mismatch.
In `@backend/global_api_deployment_key/migrations/0001_initial.py`:
- Around line 71-81: The organization FK on this model (the "organization"
ForeignKey added in the migration, inherited via DefaultOrganizationMixin) is
nullable which allows duplicate (name, NULL) rows because SQL treats NULLs as
distinct; to fix, make organization non-nullable at the model/migration level
(set null=False, blank=False and remove default=None on the organization
ForeignKey) so the existing UniqueConstraint on (name, organization) enforces
per-organization uniqueness, and update the model and migration for
GlobalAPIDeploymentKey (or the model defined in this migration) accordingly; if
you intentionally need nullable orgs instead, instead add a partial
UniqueConstraint that applies only when organization IS NOT NULL or document why
nullability is required.
In `@backend/global_api_deployment_key/models.py`:
- Around line 16-18: The BooleanField allow_all_deployments is set to
default=True which makes new keys org-wide by default; change the field
declaration to use default=False on the models.BooleanField for
allow_all_deployments (preserving db_comment), add the corresponding schema
migration to update the default, and adjust any factory/tests that relied on the
permissive default (or explicitly set allow_all_deployments=True where intended)
so behavior remains explicit.
In `@backend/global_api_deployment_key/serializers.py`:
- Around line 72-74: The api_deployments PrimaryKeyRelatedField on
GlobalApiDeploymentKeyCreateSerializer is using APIDeployment.objects.all(),
allowing cross-organization assignment; override the serializer's __init__ to
accept the request/user/org context and set
self.fields['api_deployments'].queryset =
APIDeployment.objects.filter(organization=...) (use the org from
self.context['request'].user or passed context) so only same-organization
deployments are selectable; apply the identical change to
GlobalApiDeploymentKeyUpdateSerializer and ensure both serializers still allow
required=False and many=True.
In `@backend/global_api_deployment_key/views.py`:
- Around line 67-73: In the rotate method, update_fields is preventing Django's
auto_now on modified_at from running; change the save call in
GlobalApiDeploymentKeyView.rotate (currently instance.save(update_fields=["key",
"modified_by"])) to include "modified_at" in the update_fields list
(["key","modified_by","modified_at"]) or simply call instance.save() without
update_fields so modified_at is updated automatically after rotating the key.
In `@backend/middleware/request_id.py`:
- Around line 14-17: The middleware currently forces response.status_code = 200
for any request where "/api/v1/socket" appears in request.path and thus masks
real errors and is too broad; remove the status code rewrite and tighten the
match to an explicit path or prefix check (e.g., use request.path ==
"/api/v1/socket" or request.path.startswith("/api/v1/socket/") as appropriate)
so the code simply returns the response without mutating response.status_code;
update the condition in request_id.py to use the narrowed match and delete the
assignment to response.status_code to preserve real error semantics.
In `@backend/utils/log_events.py`:
- Around line 136-138: The start_server function currently returns the raw
Django WSGIHandler which bypasses Engine.IO/Socket.IO; restore the Socket.IO
wrapper by replacing the commented line with a call that wraps django_app in
socketio.WSGIApp(sio, django_app, socketio_path=namespace) and return that
wrapper (keep WSGIHandler typing intact), removing the commented-out line so
socket routes are handled by socketio rather than Django.
In `@frontend/src/routes/useMainAppRoutes.js`:
- Around line 218-221: The new route rendering GlobalApiDeploymentKeysPage is
not protected by admin-only gating; wrap this Route (the one with path
"settings/global-api-deployment-keys" and element <GlobalApiDeploymentKeysPage
/>) with the RequireAdmin protection (or move it inside the existing
admin-protected block that uses RequireAdmin) so only admins can access it;
ensure you reference the same Route and component names and keep any existing
Route props intact while nesting it under RequireAdmin.
---
Nitpick comments:
In `@backend/api_v2/key_helper.py`:
- Around line 96-97: Update the except blocks that currently do "except
(ValueError, AttributeError): raise UnauthorizedKey()" to use explicit exception
chaining; replace the raise with "raise UnauthorizedKey() from None" (or "from
err" if you capture the exception as "except (ValueError, AttributeError) as
err:") for each place that raises UnauthorizedKey (the three except blocks that
currently raise UnauthorizedKey).
In `@backend/global_api_deployment_key/permissions.py`:
- Around line 1-3: The re-export of IsOrganizationAdmin exposes the upstream
error message mentioning “platform API keys”; create a small feature-specific
subclass (e.g., GlobalAPIDeploymentKeyIsOrganizationAdmin) that inherits from
IsOrganizationAdmin and overrides the permission error message/attribute to
mention global API deployment keys (or this endpoint) and then export that
subclass in __all__ instead of the upstream name; update any references to use
the new subclass name so the endpoint shows the correct permission message.
In `@backend/global_api_deployment_key/views.py`:
- Around line 75-81: The deployments action currently returns all APIDeployment
objects unpaginated; update the deployments method to use DRF pagination by
calling
self.paginate_queryset(APIDeployment.objects.filter(organization=organization))
and, if a page is returned, serialize that page with
ApiDeploymentMinimalSerializer and return
self.get_paginated_response(serializer.data); otherwise serialize the full
queryset and return Response(serializer.data). Keep references to the existing
method name deployments, model APIDeployment and serializer
ApiDeploymentMinimalSerializer so the change is localized.
In
`@frontend/src/components/settings/global-api-deployment-keys/GlobalApiDeploymentKeys.jsx`:
- Around line 352-382: DeploymentScopeFields is currently defined inside
GlobalApiDeploymentKeys causing it to be recreated on every render; move the
DeploymentScopeFields function out of the GlobalApiDeploymentKeys component
(define it above or in a separate file) and accept deployments as a prop (i.e.,
change signature to DeploymentScopeFields({ form, deployments })) so it uses
Form.useWatch("allow_all_deployments", form) but no longer closes over parent
scope; update all usages inside GlobalApiDeploymentKeys to pass the form and
deployments props accordingly to preserve state, focus and avoid unnecessary
remounts.
- Around line 161-175: handleToggleStatus currently allows rapid concurrent
requests which can cause race conditions; add a per-row loading guard using a
state like updatingIds (useState(new Set())) to ignore toggles when the id is
present, set the id into updatingIds before calling axiosPrivate and remove it
in finally, and pass updatingIds.has(record?.id) to the Switch component's
loading prop while keeping onChange tied to handleToggleStatus so the switch is
disabled/indicates loading during the update; ensure you still call fetchKeys()
on success and handleException on error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 52eb7baa-a489-4324-99a6-11e09058028a
⛔ Files ignored due to path filters (1)
backend/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (24)
backend/api_v2/api_deployment_views.pybackend/api_v2/deployment_helper.pybackend/api_v2/dto.pybackend/api_v2/key_helper.pybackend/api_v2/serializers.pybackend/backend/settings/base.pybackend/backend/urls_v2.pybackend/global_api_deployment_key/__init__.pybackend/global_api_deployment_key/apps.pybackend/global_api_deployment_key/migrations/0001_initial.pybackend/global_api_deployment_key/migrations/__init__.pybackend/global_api_deployment_key/models.pybackend/global_api_deployment_key/permissions.pybackend/global_api_deployment_key/serializers.pybackend/global_api_deployment_key/urls.pybackend/global_api_deployment_key/views.pybackend/middleware/request_id.pybackend/pyproject.tomlbackend/utils/log_events.pyfrontend/src/components/navigations/side-nav-bar/SideNavBar.jsxfrontend/src/components/settings/global-api-deployment-keys/GlobalApiDeploymentKeys.cssfrontend/src/components/settings/global-api-deployment-keys/GlobalApiDeploymentKeys.jsxfrontend/src/pages/GlobalApiDeploymentKeysPage.jsxfrontend/src/routes/useMainAppRoutes.js
- Revert unrelated changes: restore socketio WSGI wrapper in log_events.py and remove socket path middleware override - Security: scope api_deployments queryset to user's organization in both Create and Update serializers to prevent cross-org assignment - Move global-api-deployment-keys route inside RequireAdmin guard - Change allow_all_deployments default to False (least privilege) - Fix retrieve endpoint to return full key via DetailSerializer so copy-to-clipboard works with the actual key value - Include modified_at in rotate update_fields so timestamp updates
chandrasekharan-zipstack
left a comment
There was a problem hiding this comment.
Please add screenshots for the FE changes. Should we include a link or a hook in the existing API deployment's creation flow to easily create or use an existing global API key?
With the current changes, we'll have to hope the user stumbles upon it or we need to explicitly educate a few users that won't scale well. I suggest adding a tool tip and link to this new page from the existing API deployment creation modal
|
Two concerns with the current approach: 1. Separate table for what could be an extension of
2. Not extensible beyond deployments
At minimum, could we either generalize this model to support both resource types from the start, or scope this as a first step with a clear path to pipeline support? |
…ields - permissions.py: subclass IsOrganizationAdmin with a clearer denial message - GlobalApiDeploymentKeys.jsx: move DeploymentScopeFields to module scope and add PropTypes
# Conflicts: # backend/uv.lock # frontend/src/routes/useMainAppRoutes.js
…ope global-key profiles - Dedupe validate_safe_text/SAFE_TEXT_PATTERN into utils.input_sanitizer (was duplicated in platform_api and global_api_deployment_key serializers); add a docstring explaining the safe-char allow-list and why (reviewer ask) - api_v2: for global API keys, verify the llm_profile_id belongs to the deployment's organization before use (ProfileManager.objects is not org-scoped, so a caller could otherwise reference another org's profile). Generic 'Profile not found' avoids cross-org info disclosure (CodeRabbit) - Drop unrelated django-celery-beat 2.5.0->2.6.0 bump (reviewer ask)
The global_api_deployment_key app was branched before commit 64b06a7 marked DefaultOrganizationMixin.organization as editable=False (server- managed, never client input). After merging main, makemigrations detected model drift ('changes not yet reflected in a migration'). Add the matching AlterField migration, mirroring the same change already applied to api_v2, connector_v2, etc.
chandrasekharan-zipstack
left a comment
There was a problem hiding this comment.
Reviewed the backend auth path, the new app, and the settings page in depth, and traced the import graph, the DTO consumers, and the validate_safe_text move.
The security core holds up
Saying this first, because the rest is a list of problems:
- Cross-org isolation is sound.
GlobalApiDeploymentKey.objects.get(key=...)is deliberately not org-scoped, which makeshas_access_to_deployment()'sorganization_idcheck the entire tenant boundary — and it's there, and it fires beforeallow_all_deploymentsis consulted. organizationcan't be client-injected —editable=False, set server-side inDefaultOrganizationMixin.save().is_global_keyis derived server-side, never read from the request.- The
validate_llm_profile_idswap is correct.ProfileManager.objectsgenuinely is not org-scoped, so the new org check really is load-bearing, and the generic"Profile not found"correctly avoids confirming another org's profile exists. - Pipelines are correctly excluded —
pipeline_v2has its own same-namedDeploymentHelpercallingKeyHelper.validate_api_keydirectly. Easy to misread; worth knowing it was checked. - The fallback doesn't mask real errors.
validate_api_keyonly ever raisesUnauthorizedKey, so a DBOperationalErrorstill surfaces as a 500 rather than being swallowed into a 401. - Moving
validate_safe_textintoutils/input_sanitizer.pydid not change the exception type — that module importsValidationErrorfromrest_framework.serializers, soplatform_apistill returns 400, not 500.
Blockers
-
No tests. This PR introduces four independent cross-tenant boundaries —
has_access_to_deployment()'s org check,get_queryset()'s org filter, theapi_deploymentsqueryset scoping, andvalidate_llm_profile_id's org equality — and not one is pinned by a test. Also unverified: rotate-invalidates-old-key,is_active=Falserejection, non-admin → 403, explicit-M2M rejecting unlisted deployments, and — highest traffic of all — that a valid deployment-scoped key still authenticates after thevalidate_apirewrite. The pattern to copy ispluggable_apps/manual_review_v2/tests/test_manual_review_views.py(APITestCase+force_authenticate+patch("utils.user_context.UserContext.get_organization")). Suggested homes:global_api_deployment_key/tests/{test_models,test_views,test_serializers}.pyand a newapi_v2/tests/test_deployment_auth.py. -
The UI mints org-wide keys by default — the model defaults
allow_all_deployments=False, the create form defaults it toTrue. Inline comment on the checkbox. -
rufffails, so CI will be red:I001 global_api_deployment_key/serializers.py:1 Import block is un-sorted I001 platform_api/serializers.py:1 Import block is un-sortedBoth auto-fixable with
ruff check --fix.
Themes in the inline comments
- Guarantee logging on every auth-rejection path. Three distinct rejection reasons currently collapse to a bare
raise UnauthorizedKey()with no log line and no exception chaining. Funnel them so no path can silently reject; keep the client response generic. - Let the ORM do the UUID coercion.
keyis aUUIDField, so the hand-rolleduuid.UUID()parse duplicatesto_python— and mirroringvalidate_api_key'sexcept (DoesNotExist, ValidationError)removes the parse, the aliased import, and a latentTypeError-on-Nonehole at once. - Don't throw away the authenticated key's identity.
validate_global_api_deployment_keyreturns the key object andvalidate_apicollapses it to a bool, so nothing can answer "which global key ran this execution?". - Incoherent scopes are representable and unvalidated in both serializers.
- Squash the two migrations — new app, neither shipped, and
0002emits no SQL. - Plus: the
"shown once"docstring is false, a mislabelled "non-critical" silent catch, hardcoded colours instead of theme tokens, and aTextField(max_length=...)that Postgres ignores.
I've also left a note on the model suggesting we eventually cover ETL and TASK pipelines for consistency, with the one privilege-escalation trap that decision carries.
Two things I checked that are not bugs
Flagging these so nobody re-derives them:
- The one-time key is not lost if the clipboard write fails.
handleCopyKeyre-fetches the full plaintext key fromretrieve(). Annoying, not destructive. initialValue={true}does not clobber the edit form.setFieldsValueruns synchronously before the Modal mounts its children, and rc-field-form skipsinitialValuewhen the store already holds a value. A scoped key is not silently widened on edit. (The create default in blocker 2 is a separate, real problem.)
Nice work overall — the hard part (the tenant boundary) is right. The gaps are around it.
- serializers.py: assigning specific deployments to a global key (allow_all_deployments=False) failed with 'Invalid pk ... does not exist' for valid same-org deployments. DRF validates a many=True relation against child_relation.queryset; the code set .queryset on the ManyRelatedField wrapper (a no-op), leaving validation at APIDeployment.objects.none(). Set child_relation.queryset in both Create and Update serializers. Found via live API testing. - GlobalApiDeploymentKeys.jsx: restore apostrophe in SAFE_TEXT_REGEX/message to match backend allow-list; the copy dropped it, rejecting valid names client-side. - views.py get_serializer_class: remove unreachable create/partial_update branches.
…lidation, cleanups Frontend (GlobalApiDeploymentKeys.jsx): - BLOCKER: default 'Allow all API deployments' to unchecked and coerce to false (was checked / ?? true) so Create no longer silently mints an org-wide key; require >=1 deployment when not allow-all. Add .catch() to validateFields. Backend: - serializers: cross-field validate() rejecting incoherent scopes; fix DetailSerializer docstring (full key retrievable for lifetime, not once). - key_helper: hoist import (no circular dep), drop uuid alias, rely on UUIDField coercion instead of manual parse. - deployment_helper: log the deployment->global key auth fallback + which named global key authorized (audit visibility). - models: description TextField->CharField(512); type-annotate + note load-bearing org check on has_access_to_deployment. - migrations: squash 0002 into 0001 (app unreleased).
…urface picker load error - dto/deployment_helper: carry the resolved GlobalApiDeploymentKey through DeploymentExecutionDTO instead of a bool; is_global_key becomes a property. Preserves 'which named key authorized this execution?' for audit. - key_helper: log the reason on every global-key rejection while keeping the client 401 generic; drop the unneeded select_related(organization). - frontend: surface a failed deployments fetch (was silently swallowed).
…lobal key screens Extract the common key-management screen into a shared ApiKeyManager (table, create/edit/rotate/delete/copy, name/description + safe-text validation); the platform and global key screens become thin wrappers injecting only their feature-specific column/fields/payload shaping. Shared CSS uses antd theme tokens instead of hardcoded rgba (dark-mode aware). Kills the SonarCloud new-code duplication and the CSS theming comment.
ruff-format had collapsed the two-line error message into two adjacent string literals on one line, which reads like a forgotten comma. Merge into a single literal (same resulting message).
jaseemjaskp
left a comment
There was a problem hiding this comment.
PR Review Toolkit — consolidated findings
Ran the full toolkit (code-reviewer, silent-failure-hunter, type-design-analyzer, pr-test-analyzer, comment-analyzer, code-simplifier) over the global-API-deployment-key feature. Findings that duplicate already-posted threads (retrieve-returns-full-key, dto audit identity, cross-field scope validation, least-privilege default, serializers.py:428 profile authz, the RequireAdmin route thread, modified_at on rotate) were dropped.
The implementation itself is sound — the org-scoping guard, the UnauthorizedKey-only fallback, UUID coercion, and per-branch logging were all independently verified as correct. The one blocking item is not a defect in the code but its complete absence of tests on a security-critical, cross-tenant authorization path — shipping that untested is the merge risk. Everything else is low / nit.
Summary
- 🔴 1 blocking: no tests for the global-key auth path (cross-org rejection, authz matrix, fallback ordering, malformed key, profile org check)
- 🟡 4 low: model-layer scope invariant unenforced; partial-PATCH scope traps; DTO leaks bearer token via repr; inaccurate sanitizer comment + missing
validate_safe_texttests - ⚪ 5 nits: serializer duplication, removable
destroyoverride,validateFieldscatch, static-rules hoist / column widths, frontend transform merge
Verdict: not approving solely on the untested auth path — add the auth tests and this is good to go.
…B query Addresses the outstanding review round. Backend - Add the missing test coverage for the security-critical global-key auth path: cross-org rejection, the full allow_all/scoped/inactive matrix, deployment-key -> global-key fallback ordering, malformed-key handling, and the org-scoped llm_profile_id check. No DB needed. - Fix an import-time DB query: the class-level `queryset=APIDeployment.objects.none()` ran the org-scoped manager's get_queryset (and therefore Organization.objects.get) at module import. The field is now built in __init__, which also drops the child_relation workaround. - Fix two partial-PATCH traps: flipping a scoped key to allow-all in one field now works (and clears the M2M), and a key stranded by a deleted deployment stays editable/deactivatable. - Extract _GlobalApiDeploymentKeyWriteSerializer to de-duplicate the api_deployments field, __init__ and validate_description. - DeploymentExecutionDTO: api_key gets repr=False so the raw bearer token can't reach logs via the dataclass repr; mark the DTO frozen. - Drop the destroy() override, which reproduced ModelViewSet.destroy. - Correct the "no quotes reach storage" claim in input_sanitizer (the apostrophe is allowed) and cover validate_safe_text with tests. - Document why an unattached LLM profile fails closed for global keys, and why allow_all_deployments intentionally wins over api_deployments. Frontend - Discriminate the antd validation reject from real errors in the create/edit catch, so a non-validation throw no longer leaves the modal stuck in confirmLoading. - Hoist the static form rules out of render; trim a column width so base + one injected column totals 100%. - Merge the two payload transforms into one.
|
Review round addressed in Fixed
Found while writing the tests
The field is now built in Deliberately not changed
Stale bot findings on Tests: |
Frontend Lint Report (Biome)✅ All checks passed! No linting or formatting issues found. |
|
Unstract test resultsPer-group results
Critical paths
|





What
Why
How
Backend:
global_api_deployment_keywith model, views, serializers, URLs, and permissionsGlobalApiDeploymentKeymodel supportsallow_all_deployments(org-wide access) or explicit M2M assignment to specificAPIDeploymentinstances/api/v1/unstract/<org_id>/global-api-deployment/KeyHelper.validate_global_api_deployment_key()checks key existence, active status, org match, and deployment accessFrontend:
GlobalApiDeploymentKeyscomponent with full CRUD UI (create, edit, delete, rotate, copy key)/:orgName/settings/global-api-deployment-keysCan this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)
KeyHelperonly gets a new static method; existing key validation methods are untouched/api/v1/socketpathsDatabase Migrations
0001_initial.pycreates:global_api_deployment_keytable with UUID PK, name, description, key, is_active, allow_all_deployments, org FK, created_by FK, modified_by FKapi_deploymentsrelationshipEnv Config
Relevant Docs
Related Issues or PRs
Dependencies Versions
Notes on Testing
Screenshots
Checklist
I have read and understood the Contribution Guidelines.