8000 feat: add user verification enforcement for Console project by HarshMN2345 · Pull Request #10558 · appwrite/appwrite · GitHub
[go: up one dir, main page]

Skip to content

Conversation

HarshMN2345
Copy link
Member

What does this PR do?

(Provide a description of what this PR does and why it's needed.)

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.)

Related PRs and Issues

  • (Related PR or issue)

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?

Copy link
Contributor
coderabbitai bot commented Sep 28, 2025
📝 Walkthrough

Walkthrough

Adds a verification gate in app/controllers/shared/api.php that activates when no API key is provided, a global verification flag is on, the project is the console, and a user exists. Requests to non-allowlisted endpoints require the user to have either email or phone verified; otherwise, a USER_VERIFICATION_REQUIRED error is thrown. Introduces an end-to-end test suite validating behavior across console vs. regular projects, allowlisted endpoints, env flag control, API key bypass, and unauthenticated cases. Adds unit tests covering global and project verification flags, endpoint matching, and user verification states (email or phone).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description consists solely of the generic contributor template with empty placeholders and does not summarize or explain any of the actual changes related to user verification enforcement or the new tests. Please update the pull request description to include a brief overview of the new verification middleware, the affected endpoints, the added end-to-end and unit tests, and a clear test plan or related issue references.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly and concisely describes the primary enhancement introduced by this pull request—adding user verification enforcement for the Console project—which aligns directly with the middleware and tests implemented in the changeset.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-ser-141-require-verification

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.

Copy link
github-actions bot commented Sep 28, 2025

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
libexpat 2.7.1-r0 CVE-2025-59375 HIGH
golang.org/x/crypto v0.31.0 CVE-2025-22869 HIGH
golang.org/x/oauth2 v0.24.0 CVE-2025-22868 HIGH
stdlib 1.22.10 CVE-2025-47907 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

Copy link

✨ Benchmark results

  • Requests per second: 1,151
  • Requests with 200 status code: 207,205
  • P99 latency: 0.168519005

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,151 927
200 207,205 166,866
P99 0.168519005 0.204618273

Copy link
Contributor
@coderabbitai coderabbitai bot 8000 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: 3

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf92c25 and 768c716.

📒 Files selected for processing (3)
  • app/controllers/shared/api.php (1 hunks)
  • tests/e2e/Services/Projects/ProjectsVerificationE2ETest.php (1 hunks)
  • tests/unit/Network/Validators/VerificationTest.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/e2e/Services/Projects/ProjectsVerificationE2ETest.php (2)
tests/e2e/Client.php (1)
  • Client (8-342)
tests/e2e/Scopes/Scope.php (1)
  • Scope (12-256)
app/controllers/shared/api.php (1)
src/Appwrite/Extend/Exception.php (1)
  • Exception (7-447)
🪛 GitHub Actions: Linter
tests/e2e/Services/Projects/ProjectsVerificationE2ETest.php

[error] 1-1: PSR12: no_unused_imports

app/controllers/shared/api.php

[error] 1-1: PSR12: no_whitespace_in_blank_line

tests/unit/Network/Validators/VerificationTest.php

[error] 1-1: PSR12: no_whitespace_in_blank_line

Comment on lines +360 to +362
if (!$emailVerified && !$phoneVerified) {
throw new Exception(Exception::USER_VERIFICATION_REQUIRED);
}
Copy link
Contributor
@coderabbitai coderabbitai bot Sep 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Define USER_VERIFICATION_REQUIRED before throwing it

Line [361] throws Exception::USER_VERIFICATION_REQUIRED, but Appwrite\Extend\Exception does not currently declare that constant (see src/Appwrite/Extend/Exception.php). At runtime this will trigger a fatal error (Undefined class constant 'USER_VERIFICATION_REQUIRED') the first time an unverified console request hits the middleware. Please add the constant (and corresponding error metadata) or reuse an existing error type before merging.
Based on relevant code snippet

🤖 Prompt for AI Agents
In app/controllers/shared/api.php around lines 360 to 362, the code throws
Exception::USER_VERIFICATION_REQUIRED but that class constant is not defined,
which will cause a fatal error at runtime; fix by either adding
USER_VERIFICATION_REQUIRED to src/Appwrite/Extend/Exception.php with appropriate
error code/message/metadata consistent with other constants, or change the throw
to reuse an existing defined exception constant (e.g., a suitable
ACCESS/VALIDATION error) and ensure the error metadata aligns with how callers
expect verification errors to be represented.

Copy link
Contributor

Choose a reason for hiding this comment

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

@HarshMN2345 please see this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +16 to +107
public function testVerificationMiddlewareWithConsoleProject()
{
// Test with console project (should trigger verification check when enabled)
$this->client->setProject('console');

// Test accessing a protected endpoint without verification
// Note: This test assumes verification is enabled via environment variable
$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json'
]);

// Should either work (if verification disabled) or require verification
$this->assertContains($response['headers']['status-code'], [200, 401, 403]);
}

public function testVerificationMiddlewareWithRegularProject()
{
// Test with regular project (should not trigger verification check)
$this->client->setProject($this->getProject()['$id']);

// Test accessing a protected endpoint
$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json'
]);

// Should work normally for non-console projects
$this->assertContains($response['headers']['status-code'], [200, 401]);
}

public function testAllowedEndpointsWorkWithoutVerification()
{
// Test that allowed endpoints work without verification
$this->client->setProject('console');

$allowedEndpoints = [
'/account',
'/console/variables',
'/health/version'
];

foreach ($allowedEndpoints as $endpoint) {
$response = $this->client->call('GET', $endpoint, [], [
'content-type' => 'application/json'
]);

// Should work regardless of verification status
$this->assertNotEquals(403, $response['headers']['status-code'], "Endpoint $endpoint should be allowed without verification");
}
}

public function testEnvironmentVariableControl()
{
// Test that environment variable controls the verification system
// This test verifies the system respects the _APP_VERIFICATION_REQUIRED setting

$this->client->setProject('console');

// Test with verification potentially enabled
$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json'
]);

// Should work or require verification based on environment setting
$this->assertContains($response['headers']['status-code'], [200, 401, 403]);
}

public function testVerificationMiddlewareWithApiKey()
{
// Test that API key authentication bypasses verification
$this->client->setProject('console');

$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json',
'x-appwrite-key' => $this->getProject()['apiKey']
]);

// Should work with API key regardless of verification
$this->assertNotEquals(403, $response['headers']['status-code'], "API key should bypass verification");
}

public function testVerificationMiddlewareWithEmptyUser()
{
// Test that empty/unauthenticated users are handled properly
$this->client->setProject('console');

$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json'
]);

// Should either work or require authentication, but not verification specifically
$this->assertContains($response['headers']['status-code'], [200, 401, 403]);
}
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

E2E assertions aren’t enforcing the new behavior

Across Lines [28], [42], [79], [93], and [107] the assertions accept almost any outcome (e.g. [200, 401, 403]), so these tests will pass even if the verification gate never activates. That means a regression—like the middleware failing to block unverified console users—would go unnoticed. Please structure the scenarios so that you control _APP_VERIFICATION_REQUIRED and the user verification state, then assert the specific status codes the middleware must return (e.g. expect 403 for an unverified console user when the flag is enabled, expect 200 for allowlisted endpoints, etc.).

🤖 Prompt for AI Agents
In tests/e2e/Services/Projects/ProjectsVerificationE2ETest.php lines 16 to 107,
the assertions accept broad status arrays which mask regressions; modify each
test to explicitly set the _APP_VERIFICATION_REQUIRED environment flag and the
user's verification/auth state (mock or create a verified/unverified user or use
an API key) so the test scenario is deterministic, then replace the loose
assertContains checks with exact expectations: assertEquals(403 for an
unverified console user when verification is enabled; assertEquals(200 for
verified console user or when verification is disabled; assertEquals(200 for
allowlisted endpoints regardless of verification; assertNotEquals(403 when using
a valid API key)). Ensure setup/teardown toggles the env flag and restores it
for other tests.

Comment on lines +39 to +201
public function testVerificationMiddlewareWithGlobalDisabled()
{
// Mock environment variable
$_ENV['_APP_VERIFICATION_REQUIRED'] = 'disabled';

$verificationRequired = System::getEnv('_APP_VERIFICATION_REQUIRED', 'disabled') === 'enabled';

$this->assertFalse($verificationRequired);
}

public function testVerificationMiddlewareWithGlobalEnabled()
{
// Test the logic directly
$globalVerificationEnabled = 'enabled' === 'enabled';

$this->assertTrue($globalVerificationEnabled);
}

public function testProjectVerificationRequired()
{
$project = new Document([
'$id' => 'console',
'verificationRequired' => true
]);

$projectVerificationRequired = $project->getAttribute('verificationRequired', false);

$this->assertTrue($projectVerificationRequired);
}

public function testProjectVerificationNotRequired()
{
$project = new Document([
'$id' => 'regular-project',
'verificationRequired' => false
]);

$projectVerificationRequired = $project->getAttribute('verificationRequired', false);

$this->assertFalse($projectVerificationRequired);
}

public function testCombinedVerificationLogic()
{
// Test both global and project settings must be true
$globalVerificationEnabled = true;
$projectVerificationRequired = true;

$verificationActive = $globalVerificationEnabled && $projectVerificationRequired;

$this->assertTrue($verificationActive);
}

public function testCombinedVerificationLogicGlobalDisabled()
{
// Test global disabled overrides project setting 8000
$globalVerificationEnabled = false;
$projectVerificationRequired = true;

$verificationActive = $globalVerificationEnabled && $projectVerificationRequired;

$this->assertFalse($verificationActive);
}

public function testCombinedVerificationLogicProjectDisabled()
{
// Test project disabled overrides global setting
$globalVerificationEnabled = true;
$projectVerificationRequired = false;

$verificationActive = $globalVerificationEnabled && $projectVerificationRequired;

$this->assertFalse($verificationActive);
}

public function testAllowedEndpoints()
{
$allowedEndpoints = [
'/v1/account',
'/v1/console/variables',
'/v1/health/version',
'/v1/account/verification',
'/v1/account/verification/phone',
'/v1/account/recovery',
'/v1/account/sessions',
'/v1/account/tokens',
'/v1/account/mfa'
];

$currentPath = '/v1/account/sessions';

$isAllowedEndpoint = false;
foreach ($allowedEndpoints as $allowedEndpoint) {
if (str_starts_with($currentPath, $allowedEndpoint)) {
$isAllowedEndpoint = true;
break;
}
}

$this->assertTrue($isAllowedEndpoint);
}

public function testProtectedEndpoints()
{
$allowedEndpoints = [
'/v1/account',
'/v1/console/variables',
'/v1/health/version',
'/v1/account/verification',
'/v1/account/verification/phone',
'/v1/account/recovery',
'/v1/account/sessions',
'/v1/account/tokens',
'/v1/account/mfa'
];

$currentPath = '/v1/users';

$isAllowedEndpoint = false;
foreach ($allowedEndpoints as $allowedEndpoint) {
if (str_starts_with($currentPath, $allowedEndpoint)) {
$isAllowedEndpoint = true;
break;
}
}

$this->assertFalse($isAllowedEndpoint);
}

public function testVerifiedUserAccess()
{
$emailVerified = $this->verifiedUser->getAttribute('emailVerification', false);
$phoneVerified = $this->verifiedUser->getAttribute('phoneVerification', false);

$this->assertTrue($emailVerified || $phoneVerified);
}

public function testUnverifiedUserAccess()
{
$emailVerified = $this->unverifiedUser->getAttribute('emailVerification', false);
$phoneVerified = $this->unverifiedUser->getAttribute('phoneVerification', false);

$this->assertFalse($emailVerified || $phoneVerified);
}

public function testEmptyUserAccess()
{
$this->assertTrue($this->emptyUser->isEmpty());
}

public function testPhoneVerifiedUser()
{
$phoneVerifiedUser = new Document([
'$id' => 'user3',
'emailVerification' => false,
'phoneVerification' => true,
]);

$emailVerified = $phoneVerifiedUser->getAttribute('emailVerification', false);
$phoneVerified = $phoneVerifiedUser->getAttribute('phoneVerification', false);

$this->assertTrue($emailVerified || $phoneVerified);
}
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

Unit tests only assert hard-coded booleans

Lines [52], [84]-[111], and similar sections merely compare literal booleans (e.g. 'enabled' === 'enabled', $verificationActive = true && true). These tests never execute the actual verification middleware or any shared helper, so they cannot fail even if the production logic is broken. Please refactor the tests to exercise the real validator/middleware entry point (or a dedicated helper) with representative Document fixtures and environment settings; otherwise they give a false sense of coverage.

@HarshMN2345 HarshMN2345 removed the request for review from vermakhushboo September 28, 2025 11:26
Comment on lines +360 to +362
if (!$emailVerified && !$phoneVerified) {
throw new Exception(Exception::USER_VERIFICATION_REQUIRED);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

@HarshMN2345 please see this.

Comment on lines +101 to +103
$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json'
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't a API call that is typically done on the console project 🧐

Comment on lines +85 to +90
$this->client->setProject('console');

$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json',
'x-appwrite-key' => $this->getProject()['apiKey']
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

this makes the /users call against the console project? I would expect this to always return 401 🧐

Comment on lines +336 to +347
$allowedEndpoints = [
'/v1/account', // User account operations
'/v1/console/variables', // Console configuration
'/v1/health/version', // Health checks
'/v1/account/verification', // Email verification
'/v1/account/verification/phone', // Phone verification
'/v1/account/recovery', // Account recovery
'/v1/account/sessions', // Session management
'/v1/account/tokens', // Token management
'/v1/account/mfa' // Multi-factor authentication
];

Copy link
Contributor
@Meldiron Meldiron Oct 1, 2025

Choose a reason for hiding this comment

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

Instead of having list of endpoints here, we can do one of 2 things:

A) Have a special group() for endpoints that are supposed to be blocked (might be bad idea if its too many)
B) Have a special label() for marking endpoint as allowed without verification

Also, this would enforce email verification on self-hosted too. It might feel like extra blocker, requiring to setup SMTP, causing people to leave the onboarding.

For that reason, I think this user verification exception for Console should be cloud-specific, in Cloud repository.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think anything under /account should be accessible even if not verified

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.

3 participants

0