8000 Add impersonation feature for user management by eldadfux · Pull Request #11533 · appwrite/appwrite · GitHub
[go: up one dir, main page]

Skip to content

Add imperso 8000 nation feature for user management#11533

Open
eldadfux wants to merge 7 commits into1.8.xfrom
feat-user-impersonation
Open

Add impersonation feature for user management#11533
eldadfux wants to merge 7 commits into1.8.xfrom
feat-user-impersonation

Conversation

@eldadfux
Copy link
Member

Add impersonation feature for user management

  • Introduced a new API endpoint to update user impersonator capability.
  • Enhanced user model to include impersonator attributes.
  • Updated database schema to support impersonation.
  • Implemented impersonation logic in the request handling to allow users with impersonator capability to act as other users.
  • Added relevant API documentation for impersonation headers.

This feature allows users with the appropriate permissions to impersonate other users, enhancing flexibility in user management.

- Introduced a new API endpoint to update user impersonator capability.
- Enhanced user model to include impersonator attributes.
- Updated database schema to support impersonation.
- Implemented impersonation logic in the request handling to allow users with impersonator capability to act as other users.
- Added relevant API documentation for impersonation headers.

This feature allows users with the appropriate permissions to impersonate other users, enhancing flexibility in user management.
@coderabbitai
Copy link
Contributor
10000 coderabbitai bot commented Mar 12, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds user impersonation support: a new boolean impersonator attribute (with DB index) and API to toggle it; request handling that, when the current user has impersonator enabled and one of the headers X-Appwrite-Impersonate-User-Id, X-Appwrite-Impersonate-User-Email, or X-Appwrite-Impersonate-User-Phone is provided, selects a target user and replaces the request user context with the impersonated user while recording impersonation metadata. Updates include SDK key declarations, CORS headers, response models (Account/User), query validators, audit payloads, scope handling, a PATCH endpoint for impersonator, and extensive end-to-end tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding an impersonation feature for user management, which aligns with the comprehensive changeset across multiple files.
Description check ✅ Passed The description is directly related to the changeset, covering the key aspects: API endpoint, user model enhancements, database schema updates, impersonation logic, and API documentation.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-user-impersonation
📝 Coding Plan
  • Generate coding plan for human review comments

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 Mar 12, 2026

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
imagemagick 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-c++ 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-c++ 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-dev 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-dev 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-heic 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-heic 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-jpeg 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-jxl 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-jxl 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-libs 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-libs 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-tiff 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-tiff 7.1.2.13-r0 CVE-2026-27798 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25897 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25898 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25968 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25971 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25983 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25986 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-25987 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-26284 CRITICAL
imagemagick-webp 7.1.2.13-r0 CVE-2026-24481 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-24485 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25794 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25795 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25796 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25798 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25799 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25965 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25966 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25967 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25969 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25970 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25985 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25988 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-25989 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-26066 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-26283 HIGH
imagemagick-webp 7.1.2.13-r0 CVE-2026-27798 HIGH
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
libheif 1.20.2-r1 CVE-2025-68431 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
zlib 1.3.1-r2 CVE-2026-22184 CRITICAL
zlib-dev 1.3.1-r2 CVE-2026-22184 CRITICAL

Source Code Scan Results

🎉 No vulnerabilities found!

@greptile-apps
Copy link
greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR introduces a user impersonation feature that allows users granted the impersonator capability (via a new PATCH /v1/users/:userId/impersonator endpoint) to act as other users by passing X-Appwrite-Impersonate-User-* headers. The database schema, response models, SDK specs, and query validators are all updated accordingly. While the core idea is sound, there are two significant security issues and one documentation bug that should be addressed before merging.

  • Critical — Privilege escalation (app/controllers/shared/api.php:342-344): Every impersonated request unconditionally receives the users.read scope, which is an admin-level scope. This allows any impersonator to call endpoints like GET /v1/users that neither they nor the impersonated user would normally be allowed to access.
  • High — No restriction on impersonating privileged users (app/init/resources.php:475-486): There is no check preventing an impersonator from targeting users who have elevated roles or who themselves have impersonator: true. This can be used to chain privilege escalation.
  • Medium — Audit trail loss (app/init/resources.php:483-486): Actions performed under impersonation are attributed entirely to the target user. The impersonatorUserId is an ephemeral in-memory attribute that is never forwarded to the audit queue, making it impossible to trace impersonated actions back to the real actor.
  • Documentation bug (src/Appwrite/Utopia/Response/Model/Account.php, User.php): The impersonatorUserId field description is inverted — it stores the impersonator's ID but the description says "the current session is impersonating this user ID."
  • Style — Low-cardinality boolean index (app/config/collections/common.php:502-509): An index on the impersonator boolean field provides negligible query optimization benefit.

Confidence Score: 1/5

  • Not safe to merge — contains a privilege escalation bug that grants admin-level users.read scope to every impersonated request.
  • The unconditional users.read scope injection in shared/api.php is a concrete privilege escalation: any operator who can grant impersonator: true (requiring users.write) can then access GET /v1/users and other users.read-gated endpoints without those scopes being explicitly authorized. Combined with the lack of guards against impersonating privileged users and the missing audit trail, the feature poses material security risks in its current state.
  • app/controllers/shared/api.php (scope escalation bug) and app/init/resources.php (missing privilege/impersonator guards and audit forwarding) require the most attention before this PR can be safely merged.

Important Files Changed

Filename Overview
app/controllers/shared/api.php Adds users.read scope to every impersonated request — a privilege escalation that lets impersonators reach admin-only endpoints regardless of the target user's actual role.
app/init/resources.php Core impersonation resolution logic: looks up target user by ID/email/phone and replaces the active user context. Missing guards against impersonating privileged or other impersonator users, and audit identity is not forwarded.
app/controllers/api/users.php Adds PATCH /v1/users/:userId/impersonator endpoint to grant/revoke impersonation capability; follows existing patterns correctly (scope, audit labels, updateDocument).
app/config/collections/common.php Adds impersonator boolean field and index to the users collection; the index has very low cardinality and is unlikely to be useful for query optimization.
src/Appwrite/Utopia/Response/Model/Account.php Exposes impersonator and impersonatorUserId in the Account response model; the impersonatorUserId description is inverted — it stores the impersonator's ID, not the impersonated user's ID.
src/Appwrite/Utopia/Response/Model/User.php Same misleading impersonatorUserId description as in Account.php; otherwise mirrors the Account model changes correctly.
src/Appwrite/Platform/Tasks/Specs.php Adds the three impersonation headers (X-Appwrite-Impersonate-User-Id/Email/Phone) to all three SDK platform specs (dev, server, console); consistent and correct.
src/Appwrite/Utopia/Database/Validator/Queries/Users.php Adds impersonator to the list of allowed query attributes for users; straightforward and consistent with existing pattern.

Sequence Diagram

sequenceDiagram
    participant C as Client (Impersonator)
    participant R as resources.php (user resource)
    participant A as shared/api.php (init middleware)
    participant E as Endpoint (e.g. GET /v1/users)
    participant DB as Database

    C->>R: Request + x-appwrite-impersonate-user-id: targetId
    R->>DB: getDocument('users', impersonatorUserId) [already resolved as $user]
    Note over R: $user.impersonator == true ✓
    R->>DB: skip(getDocument('users', targetId))
    DB-->>R: $targetUser
    R->>R: $user = clone $targetUser<br/>$user->impersonatorUserId = impersonatorId
    R-->>A: $user = targetUser (+ impersonatorUserId attr)

    A->>A: Step 4: $scopes = roles['users']['scopes']
    A->>A: ⚠️ impersonatorUserId set → append 'users.read' to $scopes
    A->>A: Step 9: scope check — allowed ∩ $scopes
    Note over A: 'users.read' now in $scopes regardless<br/>of target user's actual permissions
    A->>E: authorized (with escalated scopes)
    E->>DB: action runs as targetUser
    Note over DB: accessedAt updated for targetUser<br/>Audit logs record targetUser — impersonatorId lost
Loading

Last reviewed commit: 8304a8e

Comment on lines +483 to +486
$impersonatorUserId = $user->getId();
$user = clone $targetUser;
$user->setAttribute('impersonatorUserId', $impersonatorUserId);
}
Copy link

Choose a reason for hiding this comment

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

Audit trail loses impersonator identity

After line 485, $user is the target user and impersonatorUserId lives only as a transient in-memory attribute (not a DB field). All downstream audit recording ($queueForAudits->setUser($user), $dbForProject->setMetadata('user', $user->getId()), and the accessedAt timestamp update in api.php) will attribute every action to the impersonated user's ID, with no persistent link to the actual impersonator.

This means:

  • The impersonator's own accessedAt is never bumped for impersonated requests.
  • The impersonated user's accessedAt is updated as if they made the request, making access logs misleading.
  • Audit events record userId = targetUser->getId() with no field recording impersonatorUserId.

The impersonatorUserId attribute should be passed to the audit queue explicitly so every action under impersonation can be traced back to its true actor.

@github-actions
Copy link
github-actions bot commented Mar 12, 2026

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit 8304a8e - 7 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.24s Logs
LegacyConsoleClientTest::testUniqueIndexDuplicate 1 298ms Logs
LegacyConsoleClientTest::testUpdatePermissionsWithEmptyPayload 1 240.30s Logs
TablesDBConsoleClientTest::testCreateIndexes 1 241.48s Logs
TablesDBCustomServerTest::testAttributeResponseModels 1 240.74s Logs
TablesDBCustomServerTest::testUniqueIndexDuplicate 1 240.21s Logs
TablesDBTransactionsConsoleClientTest::testDeleteDocumentDuringTransaction 1 241.04s Logs
Commit 29d9c13 - 9 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.28s Logs
LegacyCustomClientTest::testEnforceCollectionPermissions 1 240.27s Logs
TablesDBConsoleClientTest::testCreateIndexes 1 242.46s Logs
TablesDBConsoleClientTest::testListDocumentsCacheBustedByAttributeChange 1 240.06s Logs
TablesDBCustomClientTest::testListDocumentsCacheBustedByAttributeChange 1 240.43s Logs
TablesDBCustomClientTest::testUniqueIndexDuplicate 1 279ms Logs
TablesDBCustomServerTest::testTimeout 1 240.34s Logs
LegacyTransactionsCustomClientTest::testOperationsOnNonExistentDocuments 1 240.70s Logs
TablesDBTransactionsCustomClientTest::testBulkDeleteMatchingCreatedDocuments 1 240.37s Logs
Commit d8df5f1 - 2 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.22s Logs
FunctionsCustomServerTest::testCreateFunctionAndDeploymentFromTemplate 1 50.53s Logs
Commit b85cf2f - 1 flaky test
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.33s Logs
Commit f6d38fe - 2 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.27s Logs
FunctionsScheduleTest::testCreateScheduledAtExecution 1 126.42s Logs

Note: Flaky test results are tracked for the last 5 commits

@github-actions
Copy link
github-actions bot commented Mar 12, 2026

✨ Benchmark results

  • Requests per second: 1,603
  • Requests with 200 status code: 288,673
  • P99 latency: 0.106428528

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,603 1,151
200 288,673 207,254
P99 0.106428528 0.194509847

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: 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 `@app/controllers/shared/api.php`:
- Around line 341-345: The current logic appends a global 'users.read' scope
when $user->getAttribute('impersonatorUserId') is present, which escalates
impersonated sessions to project-wide user access; instead, stop adding a global
scope and grant a scoped permission limited to the impersonation target (e.g.,
create and add a per-user scope like "users.read:{targetUserId}" using
$user->getAttribute('id') or otherwise record the impersonation target in the
session/scopes), or remove adding any users.read scope and ensure downstream
authorization checks in users-related handlers validate that the active session
is only allowed to access the impersonated user's records via the impersonation
attribute.

In `@app/init/resources.php`:
- Around line 482-490: The code replaces the authenticated caller in $user with
the impersonated $targetUser; instead, preserve the original actor and introduce
an explicit effective user for impersonation. Do not reassign $user—store the
original caller id in $impersonatorUserId = $user->getId(); create an
$effectiveUser (or $actingUser) that is a clone of $targetUser and
setAttribute('impersonatorUserId', $impersonatorUserId) on that clone, then use
$effectiveUser when performing actions as the target while keeping $user for
session re-verification; also set DB metadata to record both actor and effective
IDs (e.g. keep $dbForProject->setMetadata('user', $user->getId()) and add
$dbForProject->setMetadata('effective_user', $effectiveUser->getId()), same for
$dbForPlatform) so auditing preserves the real actor.
- Around line 472-480: The code chooses $dbForPlatform when APP_MODE_ADMIN ===
$mode which causes admin (Console) requests against non-console projects to look
up impersonation targets in the wrong DB; change the $userDb assignment (the
ternary that sets $userDb) so it selects $dbForPlatform only when
$project->getId() === 'console' and otherwise uses $dbForProject (i.e., $userDb
= ($project->getId() === 'console') ? $dbForPlatform : $dbForProject) so the
impersonation lookups in the block that calls
$userDb->getAuthorization()->skip(...), $userDb->getDocument('users',
$impersonateUserId) and $userDb->findOne('users', ...) query the project users
collection for non-console projects even in admin mode.

In `@src/Appwrite/Platform/Tasks/Specs.php`:
- Around line 163-180: Update the descriptions for the ImpersonateUserId,
ImpersonateUserEmail, and ImpersonateUserPhone header specs (the array entries
keyed as 'ImpersonateUserId', 'ImpersonateUserEmail', 'ImpersonateUserPhone') to
explicitly state these headers only work on requests already authenticated as a
user who has the impersonator capability and cannot be used with X-Appwrite-Key
alone; apply the same wording change to the other identical header spec blocks
elsewhere in the file (the repeated Impersonate* entries) so all occurrences
clearly indicate they require prior user authentication with impersonator
capability.

In `@src/Appwrite/Utopia/Response/Model/Account.php`:
- Around line 119-124: The description for the 'impersonatorUserId' field in the
Account model is ambiguous; update the rule for 'impersonatorUserId' to
explicitly state whether it refers to the original actor (the user who performed
impersonation) or the impersonation target — e.g., if it's the original actor,
change the description to: "ID of the original actor who initiated impersonation
(the impersonator); present only when the session was created using
impersonation headers" — otherwise, rename the field to 'impersonatedUserId' and
update its description accordingly so it isn't redundant with $id; adjust the
'impersonatorUserId' rule in Account.php to reflect this clarified contract.

In `@src/Appwrite/Utopia/Response/Model/User.php`:
- Around line 149-154: The description for the schema rule added via
addRule('impersonatorUserId', ...) is misleading: change it to clearly state
this field holds the ID of the user performing the impersonation (the original
actor), not the impersonation target; update the 'description' value for
impersonatorUserId in the User model to something like "ID of the user
performing the impersonation (original actor); present only when the request
used impersonation headers" so it matches the runtime behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 09aa7318-817c-488c-949d-38140020e8cb

📥 Commits

Reviewing files that changed from the base of the PR and between d8f7337 and 8304a8e.

📒 Files selected for processing (8)
  • app/config/collections/common.php
  • app/controllers/api/users.php
  • app/controllers/shared/api.php
  • app/init/resources.php
  • src/Appwrite/Platform/Tasks/Specs.php
  • src/Appwrite/Utopia/Database/Validator/Queries/Users.php
  • src/Appwrite/Utopia/Response/Model/Account.php
  • src/Appwrite/Utopia/Response/Model/User.php

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 `@tests/e2e/Services/Users/UsersBase.php`:
- Around line 2635-2669: The test testListUsersFilterByImpersonator currently
only checks response shape and a single contains, so update it to actually
assert the impersonator filter: create a control user (e.g.,
$userWithoutImpersonator) with impersonator=false, call the PATCH endpoint only
for $userWithImpersonator (as already done), then for the GET with
Query::equal('impersonator',[true]) assert that every returned user in
$response['body']['users'] has 'impersonator'===true and that
$userWithImpersonator['body']['$id'] is present; for the GET with
Query::equal('impersonator',[false]) assert every returned user has
'impersonator'===false and that $userWithoutImpersonator['body']['$id'] is
present (or assert $userWithImpersonator['body']['$id'] is absent) to prove
inclusion/exclusion.
- Around line 2589-2630: The test testImpersonationUsersReadScope currently only
checks the happy path with the x-appwrite-impersonate-user-id header; add an
extra GET /users request using the same session (use $session['body']['secret']
as before) but without the 'x-appwrite-impersonate-user-id' header and assert it
returns 401 to ensure users.read is only available during active impersonation;
update the test around the existing $listHeaders/$users block in
UsersBase::testImpersonationUsersReadScope.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a01810ee-8f97-4b1f-b32f-63d074321c1f

📥 Commits

Reviewing files that changed from the base of the PR and between 8304a8e and 29d9c13.

📒 Files selected for processing (1)
  • tests/e2e/Services/Users/UsersBase.php

Comment on lines +2635 to +2669
public function testListUsersFilterByImpersonator(): void
{
$projectId = $this->getProject()['$id'];
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders());

$userWithImpersonator = $this->client->call(Client::METHOD_POST, '/users', $headers, [
'userId' => ID::unique(),
'email' => 'filter-impersonator-true@appwrite.io',
'password' => 'password',
'name' => 'Has Impersonator',
]);
$this->assertEquals(201, $userWithImpersonator['headers']['status-code']);
$this->client->call(Client::METHOD_PATCH, '/users/' . $userWithImpersonator['body']['$id'] . '/impersonator', $headers, ['impersonator' => true]);

$response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
'queries' => [
Query::equal('impersonator', [true])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['users']);
$userIds = array_column($response['body']['users'], '$id');
$this->assertContains($userWithImpersonator['body']['$id'], $userIds);

$response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
'queries' => [
Query::equal('impersonator', [false])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['users']);
}
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 | 🟡 Minor

Actually assert the impersonator filter, not just the response shape.

The true branch passes on an unfiltered list because it only checks assertContains, and the false branch never checks membership at all. Add a known false control user and assert inclusion/exclusion, or verify every returned row has the expected impersonator value.

Suggested assertions
         $userWithImpersonator = $this->client->call(Client::METHOD_POST, '/users', $headers, [
             'userId' => ID::unique(),
             'email' => 'filter-impersonator-true@appwrite.io',
             'password' => 'password',
             'name' => 'Has Impersonator',
         ]);
         $this->assertEquals(201, $userWithImpersonator['headers']['status-code']);
+
+        $controlUser = $this->client->call(Client::METHOD_POST, '/users', $headers, [
+            'userId' => ID::unique(),
+            'email' => 'filter-impersonator-false@appwrite.io',
+            'password' => 'password',
+            'name' => 'No Impersonator',
+        ]);
+        $this->assertEquals(201, $controlUser['headers']['status-code']);
+
         $this->client->call(Client::METHOD_PATCH, '/users/' . $userWithImpersonator['body']['$id'] . '/impersonator', $headers, ['impersonator' => true]);

         $response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
             'queries' => [
                 Query::equal('impersonator', [true])->toString(),
             ],
         ]);
         $this->assertEquals(200, $response['headers']['status-code']);
         $this->assertIsArray($response['body']['users']);
         $userIds = array_column($response['body']['users'], '$id');
         $this->assertContains($userWithImpersonator['body']['$id'], $userIds);
+        $this->assertNotContains($controlUser['body']['$id'], $userIds);
+        foreach ($response['body']['users'] as $user) {
+            $this->assertTrue($user['impersonator']);
+        }

         $response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
             'queries' => [
                 Query::equal('impersonator', [false])->toString(),
             ],
         ]);
         $this->assertEquals(200, $response['headers']['status-code']);
         $this->assertIsArray($response['body']['users']);
+        $userIds = array_column($response['body']['users'], '$id');
+        $this->assertContains($controlUser['body']['$id'], $userIds);
+        $this->assertNotContains($userWithImpersonator['body']['$id'], $userIds);
+        foreach ($response['body']['users'] as $user) {
+            $this->assertFalse($user['impersonator']);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BEA4
public function testListUsersFilterByImpersonator(): void
{
$projectId = $this->getProject()['$id'];
$h 4D1C eaders = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders());
$userWithImpersonator = $this->client->call(Client::METHOD_POST, '/users', $headers, [
'userId' => ID::unique(),
'email' => 'filter-impersonator-true@appwrite.io',
'password' => 'password',
'name' => 'Has Impersonator',
]);
$this->assertEquals(201, $userWithImpersonator['headers']['status-code']);
$this->client->call(Client::METHOD_PATCH, '/users/' . $userWithImpersonator['body']['$id'] . '/impersonator', $headers, ['impersonator' => true]);
$response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
'queries' => [
Query::equal('impersonator', [true])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['users']);
$userIds = array_column($response['body']['users'], '$id');
$this->assertContains($userWithImpersonator['body']['$id'], $userIds);
$response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
'queries' => [
Query::equal('impersonator', [false])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['users']);
}
public function testListUsersFilterByImpersonator(): void
{
$projectId = $this->getProject()['$id'];
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders());
$userWithImpersonator = $this->client->call(Client::METHOD_POST, '/users', $headers, [
'userId' => ID::unique(),
'email' => 'filter-impersonator-true@appwrite.io',
'password' => 'password',
'name' => 'Has Impersonator',
]);
$this->assertEquals(201, $userWithImpersonator['headers']['status-code']);
$controlUser = $this->client->call(Client::METHOD_POST, '/users', $headers, [
'userId' => ID::unique(),
'email' => 'filter-impersonator-false@appwrite.io',
'password' => 'password',
'name' => 'No Impersonator',
]);
$this->assertEquals(201, $controlUser['headers']['status-code']);
$this->client->call(Client::METHOD_PATCH, '/users/' . $userWithImpersonator['body']['$id'] . '/impersonator', $headers, ['impersonator' => true]);
$response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
'queries' => [
Query::equal('impersonator', [true])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['users']);
$userIds = array_column($response['body']['users'], '$id');
$this->assertContains($userWithImpersonator['body']['$id'], $userIds);
$this->assertNotContains($controlUser['body']['$id'], $userIds);
foreach ($response['body']['users'] as $user) {
$this->assertTrue($user['impersonator']);
}
$response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
'queries' => [
Query::equal('impersonator', [false])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['users']);
$userIds = array_column($response['body']['users'], '$id');
$this->assertContains($controlUser['body']['$id'], $userIds);
$this->assertNotContains($userWithImpersonator['body']['$id'], $userIds);
foreach ($response['body']['users'] as $user) {
$this->assertFalse($user['impersonator']);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/Services/Users/UsersBase.php` around lines 2635 - 2669, The test
testListUsersFilterByImpersonator currently only checks response shape and a
single contains, so update it to actually assert the impersonator filter: create
a control user (e.g., $userWithoutImpersonator) with impersonator=false, call
the PATCH endpoint only for $userWithImpersonator (as already done), then for
the GET with Query::equal('impersonator',[true]) assert that every returned user
in $response['body']['users'] has 'impersonator'===true and that
$userWithImpersonator['body']['$id'] is present; for the GET with
Query::equal('impersonator',[false]) assert every returned user has
'impersonator'===false and that $userWithoutImpersonator['body']['$id'] is
present (or assert $userWithImpersonator['body']['$id'] is absent) to prove
inclusion/exclusion.

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.

🧹 Nitpick comments (6)
demo/src/api.ts (2)

83-93: Consider using else if or a switch statement for mutually exclusive impersonation types.

Since ImpersonationHeader is a discriminated union where only one type can be active, the current structure evaluates all three conditions even after a match. While functionally correct, an else if chain or switch statement would be more semantically accurate and slightly more efficient.

♻️ Optional refactor using switch
-  if (impersonation.type === 'id') {
-    headers['x-appwrite-impersonate-user-id'] = impersonation.value;
-  }
-
-  if (impersonation.type === 'email') {
-    headers['x-appwrite-impersonate-user-email'] = impersonation.value;
-  }
-
-  if (impersonation.type === 'phone') {
-    headers['x-appwrite-impersonate-user-phone'] = impersonation.value;
-  }
+  switch (impersonation.type) {
+    case 'id':
+      headers['x-appwrite-impersonate-user-id'] = impersonation.value;
+      break;
+    case 'email':
+      headers['x-appwrite-impersonate-user-email'] = impersonation.value;
+      break;
+    case 'phone':
+      headers['x-appwrite-impersonate-user-phone'] = impersonation.value;
+      break;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/src/api.ts` around lines 83 - 93, The three separate if checks on
impersonation.type should be made mutually exclusive to reflect the
ImpersonationHeader discriminated union; update the logic that sets headers (the
block referencing impersonation.type and keys 'x-appwrite-impersonate-user-id',
'x-appwrite-impersonate-user-email', 'x-appwrite-impersonate-user-phone') to use
an else-if chain or a switch on impersonation.type so only one header is set per
call, leaving the rest unchanged.

102-104: Type assertion on 204 response may cause runtime issues.

Returning {} as T for 204 responses can be unsafe if T expects specific properties. Currently deleteCurrentSession uses Record<string, never> which is compatible, but if other callers expect different types, this could silently return an unexpected shape.

♻️ Consider returning undefined or void for 204 responses

One approach is to change the return type signature to indicate that 204 responses return undefined:

-export const request = async <T>({
+export const request = async <T, AllowEmpty extends boolean = false>({
   endpoint,
   projectId,
   path,
   method = 'GET',
   impersonation = { type: 'none' },
   payload,
-}: RequestOptions): Promise<T> => {
+}: RequestOptions & { allowEmpty?: AllowEmpty }): Promise<AllowEmpty extends true ? T | undefined : T> => {

Alternatively, for this demo app, the current approach is acceptable since only deleteCurrentSession hits this path and uses Record<string, never>.

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

In `@demo/src/api.ts` around lines 102 - 104, The current 204 branch returns "{}
as T" which can be unsafe; change the branch to return undefined (i.e. "return
undefined") and update the method's generic return type to reflect that 204
yields no body (e.g. Promise<T | undefined> or Promise<void> as appropriate).
Update callers such as deleteCurrentSession to accept the new undefined result
(or keep deleteCurrentSession typed as Record<string, never> if you prefer to
leave that specific caller unchanged). Ensure the central response handling
function (the block with "if (response.status === 204)") and any exported API
function signatures are updated consistently to avoid unsafe asserts.
demo/src/App.tsx (2)

21-21: localStorage access can throw in restricted contexts.

In some browsers (Safari private browsing, certain iframe sandboxes), localStorage access throws. While acceptable for a demo app, consider wrapping in try-catch for robustness.

♻️ Defensive localStorage access
-const getStoredValue = (key: string, fallback: string) => localStorage.getItem(key) ?? fallback;
+const getStoredValue = (key: string, fallback: string) => {
+  try {
+    return localStorage.getItem(key) ?? fallback;
+  } catch {
+    return fallback;
+  }
+};

Similarly for the useEffect setters on lines 33-39:

 useEffect(() => {
-  localStorage.setItem(storageKeys.endpoint, endpoint);
+  try {
+    localStorage.setItem(storageKeys.endpoint, endpoint);
+  } catch {
+    // Storage unavailable (e.g., private browsing)
+  }
 }, [endpoint]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/src/App.tsx` at line 21, Wrap all direct localStorage accesses in
try/catch to avoid thrown exceptions in restricted contexts: update
getStoredValue to catch errors and return the fallback on any exception, and
similarly wrap any localStorage.setItem/removeItem calls used inside the
component's useEffect setters so they silently fail (and optionally log) instead
of throwing; locate functions/places by the getStoredValue function and the
useEffect blocks that call localStorage.setItem/removeItem and add try/catch
around those calls, returning fallback or skipping the write on error.

96-110: Consider handling the case when type is 'none'.

The setTarget function receives ImpersonationHeader['type'] which includes 'none', but there's no explicit handling for it. While currently no caller passes 'none', adding explicit handling would make the function more defensive.

♻️ Add explicit 'none' handling
 const setTarget = (type: ImpersonationHeader['type'], user: User) => {
+  if (type === 'none') {
+    setImpersonation({ type: 'none' });
+    return;
+  }
+
   if (type === 'id') {
     setImpersonation({ type, value: user.$id });
     return;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/src/App.tsx` around lines 96 - 110, setTarget currently ignores the
'none' option from ImpersonationHeader['type']; add an explicit branch for when
type === 'none' to clear the impersonation state via setImpersonation. Update
the setTarget function to detect type === 'none' and call setImpersonation with
the appropriate "cleared" value your state expects (e.g. null/undefined or an
explicit { type: 'none', value: '' } shape) so the UI/state is defensively reset
when 'none' is passed.
demo/src/index.css (2)

69-76: Consider removing quotes around SFMono-Regular.

Stylelint prefers unquoted font family names when they don't contain spaces or special characters that require quoting. The hyphen doesn't require quotes.

♻️ Remove unnecessary quotes
-  font-family: 'SFMono-Regular', ui-monospace, monospace;
+  font-family: SFMono-Regular, ui-monospace, monospace;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/src/index.css` around lines 69 - 76, In the code block for the CSS
selector "code" in demo/src/index.css, remove the unnecessary quotes around the
font-family token 'SFMono-Regular' so the font-family declaration reads with an
unquoted SFMono-Regular followed by the fallback list (ui-monospace, monospace);
update the font-family line in the "code" rule accordingly to satisfy
stylelint's preference for unquoted font names without spaces or special
characters.

9-9: CSS keyword casing: optimizeLegibility should be lowercase.

Per CSS specification, keyword values should be lowercase. Stylelint flags this as an error.

♻️ Fix keyword case
-  text-rendering: optimizeLegibility;
+  text-rendering: optimizelegibility;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/src/index.css` at line 9, The text-rendering declaration in index.css
uses the camelCase keyword "optimizeLegibility"; update the keyword to lowercase
so the declaration reads text-rendering: optimizelegibility; to satisfy
stylelint's keyword-case rule—modify the value for the text-rendering property
in demo/src/index.css accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@demo/src/api.ts`:
- Around line 83-93: The three separate if checks on impersonation.type should
be made mutually exclusive to reflect the ImpersonationHeader discriminated
union; update the logic that sets headers (the block referencing
impersonation.type and keys 'x-appwrite-impersonate-user-id',
'x-appwrite-impersonate-user-email', 'x-appwrite-impersonate-user-phone') to use
an else-if chain or a switch on impersonation.type so only one header is set per
call, leaving the rest unchanged.
- Around line 102-104: The current 204 branch returns "{} as T" which can be
unsafe; change the branch to return undefined (i.e. "return undefined") and
update the method's generic return type to reflect that 204 yields no body (e.g.
Promise<T | undefined> or Promise<void> as appropriate). Update callers such as
deleteCurrentSession to accept the new undefined result (or keep
deleteCurrentSession typed as Record<string, never> if you prefer to leave that
specific caller unchanged). Ensure the central response handling function (the
block with "if (response.status === 204)") and any exported API function
signatures are updated consistently to avoid unsafe asserts.

In `@demo/src/App.tsx`:
- Line 21: Wrap all direct localStorage accesses in try/catch to avoid thrown
exceptions in restricted contexts: update getStoredValue to catch errors and
return the fallback on any exception, and similarly wrap any
localStorage.setItem/removeItem calls used inside the component's useEffect
setters so they silently fail (and optionally log) instead of throwing; locate
functions/places by the getStoredValue function and the useEffect blocks that
call localStorage.setItem/removeItem and add try/catch around those calls,
returning fallback or skipping the write on error.
- Around line 96-110: setTarget currently ignores the 'none' option from
ImpersonationHeader['type']; add an explicit branch for when type === 'none' to
clear the impersonation state via setImpersonation. Update the setTarget
function to detect type === 'none' and call setImpersonation with the
appropriate "cleared" value your state expects (e.g. null/undefined or an
explicit { type: 'none', value: '' } shape) so the UI/state is defensively reset
when 'none' is passed.

In `@demo/src/index.css`:
- Around line 69-76: In the code block for the CSS selector "code" in
demo/src/index.css, remove the unnecessary quotes around the font-family token
'SFMono-Regular' so the font-family declaration reads with an unquoted
SFMono-Regular followed by the fallback list (ui-monospace, monospace); update
the font-family line in the "code" rule accordingly to satisfy stylelint's
preference for unquoted font names without spaces or special characters.
- Line 9: The text-rendering declaration in index.css uses the camelCase keyword
"optimizeLegibility"; update the keyword to lowercase so the declaration reads
text-rendering: optimizelegibility; to satisfy stylelint's keyword-case
rule—modify the value for the text-rendering property in demo/src/index.css
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 04a108c1-7f7e-48ab-832a-782a02f38dbb

📥 Commits

Reviewing files that changed from the base of the PR and between 29d9c13 and e409524.

⛔ Files ignored due to path filters (6)
  • demo/package-lock.json is excluded by !**/package-lock.json
  • demo/public/favicon.svg is excluded by !**/*.svg
  • demo/public/icons.svg is excluded by !**/*.svg
  • demo/src/assets/hero.png is excluded by !**/*.png
  • demo/src/assets/react.svg is excluded by !**/*.svg
  • demo/src/assets/vite.svg is excluded by !**/*.svg
📒 Files selected for processing (19)
  • app/config/cors.php
  • app/controllers/shared/api.php
  • demo/.env
  • demo/.env.example
  • demo/.gitignore
  • demo/README.md
  • demo/eslint.config.js
  • demo/index.html
  • demo/package.json
  • demo/src/App.css
  • demo/src/App.tsx
  • demo/src/api.ts
  • demo/src/index.css
  • demo/src/main.tsx
  • demo/tsconfig.app.json
  • demo/tsconfig.json
  • demo/tsconfig.node.json
  • demo/vite.config.ts
  • tests/e2e/Services/Users/UsersBase.php
✅ Files skipped from review due to trivial changes (3)
  • demo/.env
  • demo/.gitignore
  • demo/tsconfig.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/controllers/shared/api.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

0