X Tutup
Skip to content

Authorize function execution with user JWT header#11399

Open
hmacr wants to merge 2 commits into1.8.xfrom
ser-1127
Open

Authorize function execution with user JWT header#11399
hmacr wants to merge 2 commits into1.8.xfrom
ser-1127

Conversation

@hmacr
Copy link
Contributor

@hmacr hmacr commented Feb 25, 2026

Towards SER-1127

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

The pull request updates the function execution router to support per-user permission checks. It reads an x-appwrite-user-jwt header, computes isExecutionAllowed from existing 'any' or 'guests' permissions, and if not allowed and a user JWT exists, decodes the JWT to extract userId and checks for a user:{userId} permission. If execution remains unauthorized, it throws FUNCTION_EXECUTE_PERMISSION_MISSING.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is vague and generic, only referencing a ticket number without explaining what the change does or why. Expand the description to explain the changes made, such as adding JWT-based per-user permission checks for function execution.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding JWT-based user authorization to function execution flow.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ser-1127

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.

@github-actions
Copy link

github-actions bot commented Feb 25, 2026

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
libecpg 18.1-r0 CVE-2026-2004 HIGH
libecpg 18.1-r0 CVE-2026-2005 HIGH
libecpg 18.1-r0 CVE-2026-2006 HIGH
libecpg 18.1-r0 CVE-2026-2007 HIGH
libecpg-dev 18.1-r0 CVE-2026-2004 HIGH
libecpg-dev 18.1-r0 CVE-2026-2005 HIGH
libecpg-dev 18.1-r0 CVE-2026-2006 HIGH
libecpg-dev 18.1-r0 CVE-2026-2007 HIGH
libpng 1.6.54-r0 CVE-2026-25646 HIGH
libpng-dev 1.6.54-r0 CVE-2026-25646 HIGH
libpq 18.1-r0 CVE-2026-2004 HIGH
libpq 18.1-r0 CVE-2026-2005 HIGH
libpq 18.1-r0 CVE-2026-2006 HIGH
libpq 18.1-r0 CVE-2026-2007 HIGH
libpq-dev 18.1-r0 CVE-2026-2004 HIGH
libpq-dev 18.1-r0 CVE-2026-2005 HIGH
libpq-dev 18.1-r0 CVE-2026-2006 HIGH
libpq-dev 18.1-r0 CVE-2026-2007 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2004 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2005 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2006 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2007 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

Copy link
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: 2

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

Inline comments:
In `@app/controllers/general.php`:
- Around line 374-376: Change the empty inline catch for JWTException around the
$jwt->decode($userJwt) call to a properly formatted multi-line catch block to
satisfy PSR-12/Pint: replace the inline `catch (JWTException $error) {}` with a
catch that has the opening brace on its own line and a short comment or no-op
inside (e.g., a `// ignore` comment) so the block is not empty and the
braces_position rule is satisfied.
- Around line 371-380: The code currently grants permission when a decoded token
contains userId regardless of project; after decoding the JWT via $jwt->decode
(and catching JWTException), also validate that $payload['projectId'] matches
the current project identifier before you evaluate permissions: fetch the
current project id (e.g. from the existing context or a helper like
getCurrentProjectId()) and only set $isExecutionAllowed =
in_array("user:{$userId}", $permissions) when $payload['projectId'] ===
$currentProjectId (or include a project-scoped permission check such as
in_array("project:{$currentProjectId}", $payloadPermissions) if you use
token-scoped permissions); ensure you reference $payload['projectId'], the
JWT->decode call, $userId and $isExecutionAllowed in the change.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4a32c8 and e0d1c0a.

📒 Files selected for processing (1)
  • app/controllers/general.php

Comment on lines +371 to +380
if (!$isExecutionAllowed && !empty($userJwt)) {
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
$payload = [];
try {
$payload = $jwt->decode($userJwt);
} catch (JWTException $error) {}

$userId = $payload['userId'] ?? '';
$isExecutionAllowed = \in_array("user:{$userId}", $permissions);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bind decoded JWT to the current project before permission evaluation.

Line 379 authorizes based on userId alone. Without checking JWT projectId against the current project, a valid token from another project can be reused if user:{id} matches.

🔒 Suggested fix
             if (!$isExecutionAllowed && !empty($userJwt)) {
                 $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
                 $payload = [];
                 try {
                     $payload = $jwt->decode($userJwt);
                 } catch (JWTException $error) {}
 
                 $userId = $payload['userId'] ?? '';
-                $isExecutionAllowed = \in_array("user:{$userId}", $permissions);
+                $tokenProjectId = $payload['projectId'] ?? '';
+                if (!empty($userId) && $tokenProjectId === $project->getId()) {
+                    $isExecutionAllowed = \in_array("user:{$userId}", $permissions);
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` around lines 371 - 380, The code currently
grants permission when a decoded token contains userId regardless of project;
after decoding the JWT via $jwt->decode (and catching JWTException), also
validate that $payload['projectId'] matches the current project identifier
before you evaluate permissions: fetch the current project id (e.g. from the
existing context or a helper like getCurrentProjectId()) and only set
$isExecutionAllowed = in_array("user:{$userId}", $permissions) when
$payload['projectId'] === $currentProjectId (or include a project-scoped
permission check such as in_array("project:{$currentProjectId}",
$payloadPermissions) if you use token-scoped permissions); ensure you reference
$payload['projectId'], the JWT->decode call, $userId and $isExecutionAllowed in
the change.

Copy link
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: 1

♻️ Duplicate comments (2)
app/controllers/general.php (2)

376-377: Empty catch body may still fail PSR-12 / Pint.

The block is now split over two lines but the body remains empty. Pint's braces_position and no_empty_statement rules typically require at least a comment inside an intentionally empty catch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` around lines 376 - 377, The empty catch block
for JWTException in the code (the catch (JWTException $error) { } block)
violates PSR-12/Pint; open the catch body and either handle/log/rethrow the
exception or add an explicit comment like // intentionally left blank to satisfy
no_empty_statement/braces_position rules—e.g., call a logger or add a clear
comment inside the catch for JWTException to indicate intentional swallowing or
implement appropriate handling in the surrounding method (look for the
JWTException catch near the end of the method in general.php).

379-380: Cross-project JWT reuse is still possible.

$payload['projectId'] is never compared against $project->getId(). A valid token from another project can authorize execution here when a matching user:{id} entry exists in the permissions list.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` around lines 379 - 380, Ensure the JWT payload's
projectId is validated against the current project before allowing user-based
permissions: read $payload['projectId'] and compare it to $project->getId(), and
only compute $isExecutionAllowed = in_array("user:{$userId}", $permissions) when
the project IDs match; if the payload projectId is missing or does not equal
$project->getId(), treat as not allowed (reject/false). Also normalize types
(string casts) when comparing to avoid false matches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/general.php`:
- Line 369: The execute-permission check currently only allows 'any' and
'guests' and the JWT branch only accepts 'user:{userId}', which causes functions
with the 'users' role to be rejected; update the logic that sets
$isExecutionAllowed to also consider in_array('users', $permissions) and in the
JWT branch (the block handling validated JWT and $userId) add a check for
in_array('users', $permissions) alongside the existing
in_array("user:{$userId}", $permissions) so that authenticated users with the
'users' role are allowed to execute.

---

Duplicate comments:
In `@app/controllers/general.php`:
- Around line 376-377: The empty catch block for JWTException in the code (the
catch (JWTException $error) { } block) violates PSR-12/Pint; open the catch body
and either handle/log/rethrow the exception or add an explicit comment like //
intentionally left blank to satisfy no_empty_statement/braces_position
rules—e.g., call a logger or add a clear comment inside the catch for
JWTException to indicate intentional swallowing or implement appropriate
handling in the surrounding method (look for the JWTException catch near the end
of the method in general.php).
- Around line 379-380: Ensure the JWT payload's projectId is validated against
the current project before allowing user-based permissions: read
$payload['projectId'] and compare it to $project->getId(), and only compute
$isExecutionAllowed = in_array("user:{$userId}", $permissions) when the project
IDs match; if the payload projectId is missing or does not equal
$project->getId(), treat as not allowed (reject/false). Also normalize types
(string casts) when comparing to avoid false matches.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0d1c0a and c4dda07.

📒 Files selected for processing (1)
  • app/controllers/general.php

$userJwt = $request->getHeader('x-appwrite-user-jwt', '');
$permissions = $resource->getAttribute('execute');
if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) {
$isExecutionAllowed = (\in_array('any', $permissions)) || (\in_array('guests', $permissions));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for 'users' permission usage in execute/permission contexts
rg -n "'users'" app/controllers/general.php | head -20
rg -n "users.*permission\|permission.*users" --type=php -i | head -30

Repository: appwrite/appwrite

Length of output: 170


🏁 Script executed:

# Find the actual code at lines 369-381 in app/controllers/general.php
sed -n '360,390p' app/controllers/general.php

Repository: appwrite/appwrite

Length of output: 1436


🏁 Script executed:

# Search for Appwrite permission model references or documentation in the codebase
rg -n "permission" app/controllers/general.php -B2 -A2 | head -50

Repository: appwrite/appwrite

Length of output: 832


🏁 Script executed:

# Look for 'users' role/permission in broader context (config, tests, docs)
fd -type f \( -name "*.php" -o -name "*.json" -o -name "*.md" \) | xargs rg -l "users.*permission\|permission.*users" 2>/dev/null | head -10

Repository: appwrite/appwrite

Length of output: 231


🏁 Script executed:

# Check if there are any Appwrite SDK or validation files that document permission roles
find . -name "*.php" -type f -exec grep -l "any\|guests\|user:" {} \; | head -10

Repository: appwrite/appwrite

Length of output: 27532


🏁 Script executed:

# Search for function permission tests or documentation
find . -name "*.php" -type f | xargs grep -l "execute.*permission\|permission.*execute" 2>/dev/null | head -10

Repository: appwrite/appwrite

Length of output: 573


🏁 Script executed:

# Check if 'guests' is used and how permissions are structured
rg -n "'guests'" --type=php -B2 -A2 | head -40

Repository: appwrite/appwrite

Length of output: 1335


🏁 Script executed:

# Look for permission role documentation or validation
rg -n "'any'|'guests'|'users'|'user:'" --type=php -C1 | grep -E "(permission|execute|role)" -i | head -30

Repository: appwrite/appwrite

Length of output: 3531


🏁 Script executed:

# Search for test files that might define valid execute permissions
find . -name "*test*" -o -name "*Test*" | xargs grep -l "execute.*permission\|function.*permission" 2>/dev/null | head -5

Repository: appwrite/appwrite

Length of output: 463


🏁 Script executed:

# Check if there are any validation rules or enums for permissions
rg -n "any.*guests.*users\|guests.*any.*users" --type=php | head -20

Repository: appwrite/appwrite

Length of output: 43


Functions with users execute permission are always denied via custom domain.

Line 369 only gates on any and guests. The JWT branch (lines 371–381) then only checks user:{$userId}. Appwrite's permission model includes a users role meaning "any authenticated user", and this permission is actively used in tests. A function configured with users execute permission will be unconditionally rejected for every valid-JWT request hitting this router, because neither check accounts for it.

🐛 Proposed fix
             if (!$isExecutionAllowed && !empty($userJwt)) {
                 $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
                 $payload = [];
                 try {
                     $payload = $jwt->decode($userJwt);
                 } catch (JWTException $error) {
+                    // JWT invalid or expired; keep execution denied
                 }
 
                 $userId = $payload['userId'] ?? '';
-                $isExecutionAllowed = \in_array("user:{$userId}", $permissions);
+                if (!empty($userId)) {
+                    $isExecutionAllowed = \in_array('users', $permissions)
+                        || \in_array("user:{$userId}", $permissions);
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` at line 369, The execute-permission check
currently only allows 'any' and 'guests' and the JWT branch only accepts
'user:{userId}', which causes functions with the 'users' role to be rejected;
update the logic that sets $isExecutionAllowed to also consider
in_array('users', $permissions) and in the JWT branch (the block handling
validated JWT and $userId) add a check for in_array('users', $permissions)
alongside the existing in_array("user:{$userId}", $permissions) so that
authenticated users with the 'users' role are allowed to execute.

}

$userId = $payload['userId'] ?? '';
$isExecutionAllowed = \in_array("user:{$userId}", $permissions);
Copy link
Contributor

Choose a reason for hiding this comment

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

if (!$authorization->isValid(new Input('execute', $function->getAttribute('execute')))) { // Check if user has write access to execute function
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}

Copy link
Contributor

Choose a reason for hiding this comment

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

add tests

@github-actions
Copy link

github-actions bot commented Feb 25, 2026

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit c4dda07 - 12 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.21s Logs
LegacyCustomServerTest::testUpdateAttributeEnum 1 240.43s Logs
LegacyCustomServerTest::testInvalidDocumentStructure 1 240.90s Logs
LegacyCustomServerTest::testIncrementAttribute 1 240.33s Logs
TablesDBConsoleClientTest::testInvalidDocumentStructure 1 240.52s Logs
LegacyTransactionsConsoleClientTest::testBulkOperations 1 240.53s Logs
LegacyTransactionsConsoleClientTest::testBulkDeleteMatchingUpdatedDocuments 1 240.76s Logs
LegacyTransactionsCustomClientTest::testCreateDocument 1 240.55s Logs
LegacyTransactionsCustomClientTest::testMultipleArrayOperators 1 240.39s Logs
TablesDBTransactionsConsoleClientTest::testBulkUpdate 1 240.35s Logs
TablesDBTransactionsCustomClientTest::testBulkOperations 1 240.57s Logs
TablesDBTransactionsCustomClientTest::testDoubleCommitRollback 1 240.23s Logs

@github-actions
Copy link

✨ Benchmark results

  • Requests per second: 1,822
  • Requests with 200 status code: 328,070
  • P99 latency: 0.089171257

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,822 1,200
200 328,070 216,076
P99 0.089171257 0.173774659

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.

2 participants

X Tutup