Skip to content

Feat: Public variables API#11650

Open
Meldiron wants to merge 6 commits into1.9.xfrom
feat-public-project-keys
Open

Feat: Public variables API#11650
Meldiron wants to merge 6 commits into1.9.xfrom
feat-public-project-keys

Conversation

@Meldiron
Copy link
Copy Markdown
Contributor

What does this PR do?

Public API for managing project API keys

Test Plan

  • new tests

Related PRs and Issues

x

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Removed legacy project key routes and logic from app/controllers/api/projects.php and introduced five new modular HTTP action handlers for project keys: Create, Get, Update, Delete, and XList under src/Appwrite/Platform/Modules/Project/Http/Project/Keys/. These handlers register equivalent endpoints (with new base path /v1/project/keys and aliases for /v1/projects/:projectId/keys*), include validation, authorization-aware database access, event queuing, cache purging, and response modeling. Project HTTP service was updated to register the new actions. Additionally, Key model gained an nullable expire property and accessor, one audit label in Variables Delete was adjusted, and three tests had a response-format header added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Feat: Public variables API' is misleading; the changeset implements public API endpoints for project API keys management, not variables. Rename the title to accurately reflect the main change, e.g., 'Feat: Public project keys API' or 'Feat: Project keys management endpoints'.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description states 'Public API for managing project API keys' which directly aligns with the changeset's focus on implementing CRUD endpoints for project keys.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-public-project-keys

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 26, 2026

Greptile Summary

This PR migrates the project API key CRUD operations from the legacy app/controllers/api/projects.php to the new Platform Module architecture under src/Appwrite/Platform/Modules/Project/Http/Project/Keys/, and also fixes the audit resource label for the variable delete endpoint.\n\nThe structural migration is clean and follows the same patterns already established by the Variables module. However, three of the five new Key actions ship with acknowledged-but-unimplemented security guards:\n\n- Secret exposure (Get, Update, XList): The secret field of a key is included in every response. Any API key with project.read scope can enumerate all keys and harvest their raw secrets. Three separate // TODO comments in the code note that secrets must be hidden for non-admin callers, but the guard has not been implemented.\n- Privilege escalation (Create, Update): An API key with project.write scope can create or update keys with arbitrary scopes — including scopes broader than itself. The // TODO comments in Create and Update acknowledge that the requested scopes must be constrained to the caller's own scopes, but this validation is missing.\n- Breaking change on aliased path: keyId is now a required parameter (previously optional, defaulting to unique()). Existing callers using the legacy /v1/projects/:projectId/keys alias without supplying keyId will receive a validation error.

Confidence Score: 2/5

Not safe to merge without resolving the secret-exposure and privilege-escalation issues acknowledged by the TODO comments.

The structural migration is solid and the delete/variable-fix changes are fine, but two distinct security issues (key-secret leakage to any API-key caller and scope-elevation on create/update) are explicitly flagged as incomplete TODOs in the PR itself. Shipping this to production would let one API key read secrets of all other keys in the project and create/update keys with elevated scopes.

Create.php, Get.php, Update.php, and XList.php all require security guards before merge.

Important Files Changed

Filename Overview
src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php New key-creation action migrated to Platform Module; contains an unguarded privilege-escalation vector (no scope ceiling when caller is an API key) and exposes secrets in the response without an admin-context check.
src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Get.php Returns the full key document (including secret) to any caller with project.read scope; the guard for API-key callers is a TODO and not yet implemented.
src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php Migrated update action; partial Document update follows the Variables pattern correctly, but contains the same secret-exposure and scope-elevation TODOs as Create.php.
src/Appwrite/Platform/Modules/Project/Http/Project/Keys/XList.php Migrated list action with proper project-scoped filtering and cursor pagination; returns all key secrets to any API-key caller (noted as TODO but not yet addressed).
src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Delete.php Clean migration of the delete action; validates project ownership before deletion and correctly emits audit/event params.
src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php Correct bug-fix: audit resource label changed from {response.$id} (unavailable after deletion) to {request.variableId}.
src/Appwrite/Platform/Modules/Project/Services/Http.php Registers the five new Key actions in the correct order alongside existing Variable actions.
app/controllers/api/projects.php Removes the old legacy key CRUD routes now replaced by the Platform Module equivalents; unused imports also cleaned up.

Comments Outside Diff (2)

  1. src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php, line 412 (link)

    P1 Privilege escalation via unvalidated scopes

    When the caller is authenticated as an API key (instead of admin), the new key is created with whatever scopes the caller supplies — there is no check that those scopes are a subset of the calling key's own scopes. A key with only users.read could, for example, create a new key with users.write plus every other scope, effectively elevating its own privileges.

    The same issue is present in Update.php (line 99). Both endpoints must validate that the requested scopes do not exceed the authenticated key's own scopes before writing to the database.

  2. src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php, line 384 (link)

    P2 keyId is now required — potential backward-compat break on the aliased path

    The old controller marked keyId as optional (defaulting to unique()) with the comment // TODO: When migrating to Platform API, mark keyId required for consistency. The migration has happened here but the alias /v1/projects/:projectId/keys still exists. Any existing SDK client or script calling this path without keyId will now receive a validation error. Consider confirming this is an intentional breaking change and documenting it in the migration notes.

Reviews (1): Last reviewed commit: "Public keys Apis" | Re-trigger Greptile

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 26, 2026

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit cbfdd27 - 2 flaky tests
Test Retries Total Time Details
RealtimeConsoleClientTest::testCreateDeployment 1 2.11s Logs
UsageTest::testVectorsDBStats 1 10.18s Logs
Commit 7371b68 - 2 flaky tests
Test Retries Total Time Details
UsageTest::testVectorsDBStats 1 10.45s Logs
WebhooksCustomServerTest::testUpdateCollection 1 18.44s Logs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php`:
- Around line 65-82: The param validator currently allows scopes to be null
(Nullable in the param definition) but action(string $keyId, string $name, array
$scopes, ?string $expire, ...) requires array, causing a TypeError for "scopes":
null; update the action signature to accept ?array $scopes (and update the
phpdoc `@param` to array<string>|null) and then normalize inside the action (e.g.,
$scopes = $scopes ?? []) before any use, ensuring callers that pass null get a
4xx validation response flow rather than a TypeError.
- Around line 91-105: When the caller is authenticated via an API key
(AuthType::KEY), enforce that the new key's scopes ($scopes) are a subset of the
caller key's scopes and that the requested expiration ($expire) is not later
than the caller key's expiration; implement this check before constructing the
new Document (where $key = new Document([...])) and either trim/reject scopes or
reject the request with an appropriate error, and similarly reject expirations
that exceed the caller's expire (or alternatively disallow AuthType::KEY
entirely on this endpoint). Also apply the same enforcement to the subsequent
key-creation code path referenced around the second creation block previously
noted.

In `@src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Get.php`:
- Around line 72-74: The endpoint currently returns full key objects including
their secret; update the logic in Get.php (the handler that eventually calls
$response->dynamic($key, Response::MODEL_KEY)) to detect if the caller's auth
type is AuthType::KEY and, if so, remove/scrub the secret from $key (e.g. unset
$key['secret'] or null out the secret property) before calling
$response->dynamic($key, Response::MODEL_KEY); alternatively you may reject
key-authenticated access by returning a 403 when auth type equals AuthType::KEY
— implement one of these two options and ensure the check happens immediately
before the $response->dynamic(...) call.

In `@src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php`:
- Around line 63-77: The param definition for 'scopes' (the ->param('scopes',
null, new Nullable(new ArrayList(...))) call) allows null but the action method
signature public function action(..., array $scopes, ?string $expire, ...)
requires an array, causing a TypeError when null is passed; either remove the
Nullable wrapper so the validator disallows null (keep ->param('scopes', null,
new ArrayList(...))) or change the action signature to accept ?array $scopes and
add explicit handling inside action (e.g., treat null as [] or validate/throw)
to ensure consistent behavior between the validator and the action method.
- Around line 90-110: When the request is authenticated with an API key, ensure
the update cannot widen scopes or extend expiry beyond the caller key: before
calling $dbForPlatform->updateDocument('keys', ...) validate and clamp $scopes
and $expire against the caller key's allowed scopes/expiry (use the current auth
context via $authorization and the caller $key metadata) and reject if the
requested values exceed them; after updating, if the request AuthType is
AuthType::KEY, remove or redact the secret field from the returned $key (before
calling $response->dynamic($key, Response::MODEL_KEY)) so secrets are never
revealed to key-authenticated callers.

In `@src/Appwrite/Platform/Modules/Project/Http/Project/Keys/XList.php`:
- Around line 122-127: The list response currently returns full key documents
and leaks secrets to key-authenticated callers; before calling
response->dynamic(...) in XList (where $keys and $total are prepared and
MODEL_KEY_LIST is used), detect if the request auth type equals AuthType::KEY
and, if so, iterate $keys and remove or replace the 'secret' field (e.g., unset
or set to null/redacted) on each Document entry so key-authenticated callers
receive no raw secrets; ensure this redaction happens prior to constructing the
Document passed to response->dynamic.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ab8a547f-6b90-4ce6-ac84-042cd2a43442

📥 Commits

Reviewing files that changed from the base of the PR and between 9353fbf and cbfdd27.

📒 Files selected for processing (8)
  • app/controllers/api/projects.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Delete.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Get.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Keys/XList.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php
  • src/Appwrite/Platform/Modules/Project/Services/Http.php
💤 Files with no reviewable changes (1)
  • app/controllers/api/projects.php

@blacksmith-sh

This comment has been minimized.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 26, 2026

✨ Benchmark results

  • Requests per second: 1,467
  • Requests with 200 status code: 264,032
  • P99 latency: 0.118762327

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,467 1,083
200 264,032 194,928
P99 0.118762327 0.207953784

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Appwrite/Auth/Key.php (1)

183-192: ⚠️ Potential issue | 🔴 Critical

Wrap DateTime::addSeconds with DateTime::formatTz to return an ISO 8601 string.

Line 185 must format the datetime object to match the ?string type expected by the $expire parameter. The pattern throughout the codebase consistently uses DateTime::formatTz(DateTime::addSeconds(...)) when storing expiration values. Change to:

DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 86400))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Auth/Key.php` around lines 183 - 192, The expiry value passed to
the Key constructor uses DateTime::addSeconds(...) which returns a DateTime
object but the $expire parameter expects a ?string; wrap the call with
DateTime::formatTz so it returns an ISO8601 string. Locate the call using
DateTime::addSeconds(new \DateTime(), 86400) in Key (around where the
constructor is invoked) and replace it with
DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 86400)) so the stored
expiration matches the codebase pattern and the ?string type.
🧹 Nitpick comments (2)
src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php (1)

112-116: Consider defaulting scopes to empty array when null.

Same concern as in Create.php: if $scopes is null, it may cause issues if other parts of the codebase expect an array when reading key scopes.

         $updates = new Document([
             'name' => $name,
-            'scopes' => $scopes,
+            'scopes' => $scopes ?? [],
             'expire' => $expire,
         ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php` around
lines 112 - 116, The document update is setting 'scopes' directly from $scopes
which may be null; ensure $scopes defaults to an empty array when null before
creating the Document (e.g., normalize $scopes = $scopes ?? []), so the 'scopes'
field always contains an array in the Document instantiation in Update.php (the
$scopes variable and the new Document([...]) call).
src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php (1)

110-122: Add null coalescing to default scopes to empty array when storing in Document.

The $scopes parameter is nullable (line 66, 83) and can be null. While line 97 already handles this with $scopes ?? [], line 117 stores the raw value directly. This creates inconsistency with how the rest of the codebase retrieves scopes—using getAttribute('scopes', []) with a default empty array—and with migration logic that normalizes empty scopes to arrays. Normalize at storage time:

 'scopes' => $scopes,
+'scopes' => $scopes ?? [],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php` around
lines 110 - 122, The Document creation for $key stores 'scopes' using the
nullable $scopes variable; change the 'scopes' entry in the new Document (the
$key = new Document([...]) block) from 'scopes' => $scopes to 'scopes' =>
$scopes ?? [] so scopes are normalized to an empty array at storage time (refer
to the $key Document creation and the $scopes parameter used earlier).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/Appwrite/Auth/Key.php`:
- Around line 183-192: The expiry value passed to the Key constructor uses
DateTime::addSeconds(...) which returns a DateTime object but the $expire
parameter expects a ?string; wrap the call with DateTime::formatTz so it returns
an ISO8601 string. Locate the call using DateTime::addSeconds(new \DateTime(),
86400) in Key (around where the constructor is invoked) and replace it with
DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 86400)) so the stored
expiration matches the codebase pattern and the ?string type.

---

Nitpick comments:
In `@src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php`:
- Around line 110-122: The Document creation for $key stores 'scopes' using the
nullable $scopes variable; change the 'scopes' entry in the new Document (the
$key = new Document([...]) block) from 'scopes' => $scopes to 'scopes' =>
$scopes ?? [] so scopes are normalized to an empty array at storage time (refer
to the $key Document creation and the $scopes parameter used earlier).

In `@src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php`:
- Around line 112-116: The document update is setting 'scopes' directly from
$scopes which may be null; ensure $scopes defaults to an empty array when null
before creating the Document (e.g., normalize $scopes = $scopes ?? []), so the
'scopes' field always contains an array in the Document instantiation in
Update.php (the $scopes variable and the new Document([...]) call).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c6b33ea9-028a-4299-8bde-1a0f63551205

📥 Commits

Reviewing files that changed from the base of the PR and between cbfdd27 and 7371b68.

📒 Files selected for processing (6)
  • src/Appwrite/Auth/Key.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Create.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Get.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Update.php
  • src/Appwrite/Platform/Modules/Project/Http/Project/Keys/XList.php
  • tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
✅ Files skipped from review due to trivial changes (1)
  • tests/e2e/Services/Projects/ProjectsConsoleClientTest.php

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