Skip to content

[4.x] Parameter validation and other DB manager improvements#1459

Open
lukinovec wants to merge 62 commits intomasterfrom
validate-sql-parameters
Open

[4.x] Parameter validation and other DB manager improvements#1459
lukinovec wants to merge 62 commits intomasterfrom
validate-sql-parameters

Conversation

@lukinovec
Copy link
Copy Markdown
Contributor

@lukinovec lukinovec commented Apr 29, 2026

Parameter validation

In statement() calls of TenantDatabaseManagers, use parameter binding when possible. If that's not possible, validate the parameters using validateParameter() or validatePassword().

Passwords use a less strict allowlist than other parameters (e.g. DB names), since passwords are always quoted in the SQL statements (and since passwords tend to use special characters).

In SQLiteDatabaseManager, names of file-based databases are validated (in createDatabase, deleteDatabase and databaseExists) using a similar allowlist to the (non-password) parameters in other DB managers, with an additional character: . (this addition is necessary since file-based SQLite databases end with .sqlite).

DatabaseTenancyBootstrapper - harden() and the lost test file

While checking for more places that could use validation, I realized that it's possible to update tenant's db_name to the central DB or the DB of another tenant. Added the DatabaseTenancyBootstrapper::$harden property -- setting it to true prevents tenants from connecting to the wrong databases (RuntimeException is thrown after connecting to the wrong database).

Also, the DatabaseTenancyBootstrapper test file was ignored while running tests because it lacked the Test suffix. Added the suffix and fixed the broken DATABASE_URL test in the file.

SQLiteDatabaseManager - respect static $path property in makeConnectionConfig()

SQLiteDatabaseManager had a bug in makeConnectionConfig: the method didn't respect the static $path property, it used database_path() instead. Added a regression test for that. Also recognizing in-memory SQLite databases (using isInMemory()) is more strict now so that simply having a db_name with _tenancy_inmemory_ anywhere doesn't e.g. make a file-based database considered in-memory.

MySQLDatabaseManager - charset and collation defaulting

Creating databases with null charsets and collations resulted in a QueryException, since null isn't a valid charset/collation. To solve that, in the CREATE DATABASE statement in MySQLDatabaseManager, only add charset/collation to the statement if they are not null.

MySQL defaults to the server's charset and collation, so it's safe to not pass any charset/collation in the CREATE DATABASE statement and let MySQL choose. Also, if e.g. collation is non-null and charset is null, MySQL will use a charset compatible with the used collation, and this works both ways.

To-dos

Summary by CodeRabbit

  • New Features

    • Opt-in hardening mode to block tenants from using the central DB and auto-revert on error.
    • Centralized allowlist validation for database names, usernames, and passwords; stricter SQLite in-memory/path handling.
  • Bug Fixes

    • Validate inputs before database operations and use parameterized/quoted SQL to prevent unsafe interpolation.
  • Tests

    • Expanded validation, SQLite, and bootstrapper hardening tests; removed an outdated bootstrapper suite.

lukinovec and others added 11 commits April 29, 2026 10:08
This is just for consistency, since all the other DB managers use select().
DB manager methods validate the parameters they use in SQL statements using validateParameter() (excluding parameters passed via bindings in SELECT statements).
WIP: password validation, SQLite (check if validation is enough for valid FS paths), revisit the test
Also, use parameterAllowlist() instead of the static property (so that we can e.g. override it later in SQLiteDatabaseManager, since overriding the static property doesn't work).
Also stop skipping the validation test for sqlite.
Also, make the validateParameter method ignore null parameters, e.g. for cases when tenants are created using Tenant::make() without tenancy_db_username set -- $databaseConfig->getUsername() allows null, same should go for the validate method whose only concern is checking strings for invalid characters.
@lukinovec
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

❌ Patch coverage is 99.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 86.27%. Comparing base (9849119) to head (d9ae274).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
...ionControlledMicrosoftSQLServerDatabaseManager.php 88.88% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1459      +/-   ##
============================================
+ Coverage     85.84%   86.27%   +0.42%     
- Complexity     1163     1184      +21     
============================================
  Files           185      186       +1     
  Lines          3399     3482      +83     
============================================
+ Hits           2918     3004      +86     
+ Misses          481      478       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57d75795-9a6a-4103-987c-7ac9c24bc4eb

📥 Commits

Reviewing files that changed from the base of the PR and between 649c802 and d9ae274.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.php

📝 Walkthrough

Walkthrough

Adds a validation trait that enforces allowlisted characters for database identifiers and passwords; applies it across tenant DB managers and user-management concerns; replaces SQL interpolation with parameterized queries and safer heredocs; tightens SQLite name/in-memory checks and path handling; and adds an opt‑in bootstrapper hardening mode with tests. (34 words)

Changes

Validation trait + manager updates

Layer / File(s) Summary
New trait
src/Database/Concerns/ValidatesDatabaseParameters.php
Adds allowlists and validateParameter / validatePassword methods to enforce character-level validation.
Trait adoption
src/Database/TenantDatabaseManagers/TenantDatabaseManager.php
Imports/uses the new ValidatesDatabaseParameters trait so managers can call validation helpers.
Data extraction / normalization
src/Database/TenantDatabaseManagers/*, src/Database/Concerns/ManagesPostgresUsers.php
Capture tenant database/schema names and usernames into local variables ($name, $database, $username) before validation and SQL construction.
Core validation calls
src/Database/Concerns/ManagesPostgresUsers.php, src/Database/TenantDatabaseManagers/*
Insert upfront calls to validateParameter and validatePassword in create/delete/grant flows for database names, usernames, and passwords.
SQL hardening
src/Database/TenantDatabaseManagers/*, src/Database/Concerns/ManagesPostgresUsers.php
Replace string-interpolated SQL with parameterized queries (? bindings), switch to non-interpolating heredocs where applicable, and quote identifiers in some Postgres statements.
SQLite-specific changes
src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php
Add allowedDatabaseNameCharacters() and validateDatabaseName(), stricter isInMemory() checks, validate names in getPath() and makeConnectionConfig(), and reject empty or directory-like names.
Tests
tests/TenantDatabaseManagerTest.php
Add cross-manager parameter-validation tests (branching for user/non-user managers), SQLite name/in-memory/path tests, and MySQL charset/collation tests.

Bootstrapper hardening

Layer / File(s) Summary
Feature flag & wiring
src/Bootstrappers/DatabaseTenancyBootstrapper.php
Add public static bool $harden = false; when enabled, bootstrap() calls harden() and calls revert() on failure.
Harden checks
src/Bootstrappers/DatabaseTenancyBootstrapper.php
Implement harden(Tenant $tenant) that checks current DB name against other tenants' tenancy_db_name and uses Schema::hasTable() to detect central DB usage, throwing RuntimeException on conflict.
Tests added / removed
tests/Bootstrappers/DatabaseTenancyBootstrapperTest.php, tests/Bootstrappers/DatabaseTenancyBootstrapper.php
Add Pest tests covering hardening behavior and scenarios; remove the previous bootstrapper test file.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Bootstrapper
    participant DBManager
    participant Validator
    participant TenantsStore
    participant DB

    Client->>Bootstrapper: initialize(tenant)
    Bootstrapper->>DBManager: connectToTenant(tenant)
    DBManager->>Validator: validateParameter(database, username, schema)
    alt validation fails
        Validator-->>DBManager: InvalidArgumentException
        DBManager-->>Bootstrapper: propagate exception
        Bootstrapper->>DB: revert()
        Bootstrapper-->>Client: exception
    else validation ok
        Bootstrapper->>TenantsStore: find tenants by tenancy_db_name (if harden enabled)
        TenantsStore-->>Bootstrapper: conflict / no conflict
        alt conflict found
            Bootstrapper->>DB: revert()
            Bootstrapper-->>Client: RuntimeException
        else no conflict
            DBManager->>DB: execute parameterized SQL (CREATE/GRANT/DROP/SELECT)
            DB-->>DBManager: result
            DBManager-->>Client: success
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I checked each name, I barred each sign,
I kept the SQL tidy, every bind in line.
I guarded memory files and paths that roam,
I hardened the hop between tenant and home.
A little rabbit patch — secure as stone.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[4.x] Parameter validation and other DB manager improvements' directly summarizes the main changes: adding parameter validation across database managers and fixing related issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch validate-sql-parameters

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

Inline comments:
In `@src/Database/Concerns/ValidatesDatabaseParameters.php`:
- Around line 61-63: The loop inside validateParameter currently calls
str_split($parameter) without guarding against null array elements, causing a
TypeError when callers pass arrays containing null (e.g., results of
getUsername()/getPassword()); update validateParameter to skip any null (or
non-string) elements before calling str_split—check each $parameter with
is_string($parameter) (or continue when $parameter === null) so only strings are
iterated, and keep existing handling for top-level nulls intact.

In `@src/Database/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php`:
- Around line 13-23: The code assigns the union-typed return of
validateParameter() to $database and then interpolates it into SQL, causing
PHPStan type errors; instead, call validateParameter(...) for
side-effect/validation but keep $database as the original string from the tenant
and use that when calling connection()->statement in createDatabase and
deleteDatabase (i.e., do not reassign $database to the validateParameter()
result—validate in-place and use $tenant->database()->getName() stored as the
string $database before interpolation). Ensure you update both createDatabase
and deleteDatabase (and any other methods using validateParameter() assignment)
so connection()->statement("CREATE DATABASE [{$database}]") / statement("DROP
DATABASE [{$database}]") interpolate a known string variable.

In
`@src/Database/TenantDatabaseManagers/PermissionControlledPostgreSQLSchemaManager.php`:
- Around line 39-45: The primary-key lookup query in
PermissionControlledPostgreSQLSchemaManager is missing a schema filter and can
collide across schemas; update the SELECT against
information_schema.key_column_usage to include "table_schema = ?" and bind the
existing $schema variable alongside $tableName so the query uses both parameters
when obtaining $primaryKey (keep the query against
information_schema.key_column_usage and the use of
$this->connection()->selectOne intact).

In `@src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php`:
- Around line 95-96: databaseExists currently calls getPath($name) with raw
input, leaving path traversal/existence probing unvalidated; add a call to
$this->validateParameter($name) at the start of databaseExists() so the name is
validated before passing to getPath(), and similarly ensure any other methods
that call getPath() (e.g., createDatabase(), deleteDatabase()) perform
validateParameter() first or reuse a validated helper; update databaseExists()
to return after validation and then compute $path = $this->getPath($name).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c3c813de-1e9a-4645-a9a9-dea7a7490e2e

📥 Commits

Reviewing files that changed from the base of the PR and between 9849119 and 8592949.

📒 Files selected for processing (13)
  • src/Database/Concerns/ManagesPostgresUsers.php
  • src/Database/Concerns/ValidatesDatabaseParameters.php
  • src/Database/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/MySQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledMicrosoftSQLServerDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledPostgreSQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledPostgreSQLSchemaManager.php
  • src/Database/TenantDatabaseManagers/PostgreSQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php
  • src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php
  • src/Database/TenantDatabaseManagers/TenantDatabaseManager.php
  • tests/TenantDatabaseManagerTest.php

Comment thread src/Database/Concerns/ValidatesDatabaseParameters.php Outdated
Comment thread src/Database/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php Outdated
Comment thread src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php Outdated
lukinovec and others added 6 commits April 30, 2026 09:15
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Add validation so that a malicious tenant DB name can't be used to detect if a file exists.
Also cover that in-memory db names aren't validated in databaseExists
@lukinovec lukinovec marked this pull request as ready for review April 30, 2026 12:39
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@src/Database/Concerns/ValidatesDatabaseParameters.php`:
- Around line 67-70: The current exception thrown in the character-validation
loop inside ValidatesDatabaseParameters (the foreach over str_split and the
InvalidArgumentException) exposes the full rejected parameter; change the
exception so it does not include the raw $parameter (used by
validatePassword()), e.g. log or throw a message that references only the
invalid character and a non-sensitive identifier (parameter name or masked
length) instead of the plaintext value — update the throw in the loop to include
the offending $char and either the parameter key/type or a masked representation
(e.g. "<redacted>" or asterisks by length) rather than {$parameter}.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c919e0dc-5dd6-40e4-be94-13cc6164718d

📥 Commits

Reviewing files that changed from the base of the PR and between 8592949 and 50ea524.

📒 Files selected for processing (10)
  • src/Database/Concerns/ManagesPostgresUsers.php
  • src/Database/Concerns/ValidatesDatabaseParameters.php
  • src/Database/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/MySQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledMicrosoftSQLServerDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PostgreSQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php
  • src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php
  • tests/TenantDatabaseManagerTest.php

Comment thread src/Database/Concerns/ValidatesDatabaseParameters.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@src/Database/Concerns/ValidatesDatabaseParameters.php`:
- Line 67: The loop using foreach (str_split($parameter) as $char) in
ValidatesDatabaseParameters (method that iterates parameters) doesn't guard
against non-string, non-null values and will raise a TypeError; before calling
str_split in that loop check that $parameter is a string (e.g.,
is_string($parameter)) and if not throw the same InvalidArgumentException used
elsewhere (or coerce to string if intended), so validation stays
deterministic—update the parameter iteration in the ValidatesDatabaseParameters
class to explicitly handle non-string values and only call str_split when
$parameter is a string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dbfa5fdd-7921-4ad8-a166-0200519f5cb3

📥 Commits

Reviewing files that changed from the base of the PR and between 50ea524 and bacbf93.

📒 Files selected for processing (2)
  • src/Database/Concerns/ValidatesDatabaseParameters.php
  • tests/TenantDatabaseManagerTest.php

Comment thread src/Database/Concerns/ValidatesDatabaseParameters.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/Database/Concerns/ValidatesDatabaseParameters.php (1)

59-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-string array entries instead of skipping them.

This silently bypasses validation for mixed inputs, so values like getConfig() results that are not strings can still flow into SQL/file-path construction unvalidated. That undermines the hardening this trait is meant to provide.

Proposed fix
         foreach ((array) $parameters as $parameter) {
-            if (! is_string($parameter)) {
-                // Skip if there's nothing to validate
-                // (e.g. when $tenant->database()->getUsername() of an
-                // improperly created tenant is null and it gets passed).
-                continue;
-            }
+            if (! is_string($parameter)) {
+                throw new InvalidArgumentException('Database parameters must be strings or null.');
+            }
 
             foreach (str_split($parameter) as $char) {
                 if (! str_contains($allowlist, $char)) {
                     throw new InvalidArgumentException("Forbidden character '{$char}' in database parameter.");
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Concerns/ValidatesDatabaseParameters.php` around lines 59 - 65,
In the ValidatesDatabaseParameters trait, stop silently skipping non-string
entries in the foreach over (array) $parameters; instead validate each
$parameter and throw an appropriate exception (e.g., InvalidArgumentException)
when a non-string is encountered so mixed-type inputs are rejected; update the
loop that currently does "if (! is_string($parameter)) { continue; }" to throw
with a clear message referencing the offending parameter/value and that
$parameters must contain only strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/Database/Concerns/ValidatesDatabaseParameters.php`:
- Around line 59-65: In the ValidatesDatabaseParameters trait, stop silently
skipping non-string entries in the foreach over (array) $parameters; instead
validate each $parameter and throw an appropriate exception (e.g.,
InvalidArgumentException) when a non-string is encountered so mixed-type inputs
are rejected; update the loop that currently does "if (! is_string($parameter))
{ continue; }" to throw with a clear message referencing the offending
parameter/value and that $parameters must contain only strings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0ae3e29-793b-4ca6-baaf-4f0625b2a076

📥 Commits

Reviewing files that changed from the base of the PR and between bacbf93 and 37a4c7d.

📒 Files selected for processing (1)
  • src/Database/Concerns/ValidatesDatabaseParameters.php

@lukinovec
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/Database/TenantDatabaseManagers/PermissionControlledPostgreSQLDatabaseManager.php (1)

37-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Quote the schema name in the GRANT statements.

validateParameter() still allows schema names that require quoting in PostgreSQL, but these statements emit {$schema} unquoted. That will fail for valid mixed-case or hyphenated schemas and can target the wrong object.

Suggested fix
-        $this->connection()->statement("GRANT USAGE, CREATE ON SCHEMA {$schema} TO \"{$username}\"");
+        $this->connection()->statement("GRANT USAGE, CREATE ON SCHEMA \"{$schema}\" TO \"{$username}\"");

-        $this->connection()->statement("GRANT USAGE ON ALL SEQUENCES IN SCHEMA {$schema} TO \"{$username}\"");
+        $this->connection()->statement("GRANT USAGE ON ALL SEQUENCES IN SCHEMA \"{$schema}\" TO \"{$username}\"");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Database/TenantDatabaseManagers/PermissionControlledPostgreSQLDatabaseManager.php`
around lines 37 - 40, The GRANT statements use unquoted {$schema} which breaks
for mixed-case or special-character schemas; update the two calls in
PermissionControlledPostgreSQLDatabaseManager that invoke
$this->connection()->statement("GRANT ... IN SCHEMA {$schema} ...") to properly
quote the schema identifier (using PostgreSQL identifier quoting, e.g. wrap the
schema name in double quotes and escape any internal quotes) so the SQL targets
the correct schema when $schema contains mixed case or hyphens; ensure both the
"GRANT USAGE, CREATE ON SCHEMA" and the "GRANT USAGE ON ALL SEQUENCES IN SCHEMA"
statements are fixed and that any escaping logic for $schema is centralized or
reused.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/Database/TenantDatabaseManagers/PermissionControlledPostgreSQLSchemaManager.php`:
- Around line 23-28: The GRANT CONNECT SQL emits the central database name
unquoted which breaks for valid PostgreSQL names; update the statement in
PermissionControlledPostgreSQLSchemaManager so the database identifier is a
properly quoted PostgreSQL identifier (escape any internal double quotes by
replacing " with "" and wrap the result in double quotes) before passing it into
$this->connection()->statement("GRANT CONNECT ON DATABASE ... TO
\"{$username}\""); keep using validateParameter([$username, $schema, $database])
but substitute the safely quoted database identifier when building the GRANT
string.

In `@src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php`:
- Around line 63-66: parameterAllowlist() currently permits '.' so inputs
containing '.' or '..' segments can bypass validateParameter() and enable path
traversal later in database_path()/getPath() used by createDatabase(),
deleteDatabase(), and databaseExists(); update the validation to explicitly
reject single-dot and double-dot path segments before joining/building the path
(either in validateParameter() or immediately where segments are split), i.e.,
after splitting the input on path separators ensure no segment equals '.' or
'..' and return/throw on detection, and add a regression test that asserts '..'
is rejected (and that attempts to create/delete a database with '..' fail).

---

Outside diff comments:
In
`@src/Database/TenantDatabaseManagers/PermissionControlledPostgreSQLDatabaseManager.php`:
- Around line 37-40: The GRANT statements use unquoted {$schema} which breaks
for mixed-case or special-character schemas; update the two calls in
PermissionControlledPostgreSQLDatabaseManager that invoke
$this->connection()->statement("GRANT ... IN SCHEMA {$schema} ...") to properly
quote the schema identifier (using PostgreSQL identifier quoting, e.g. wrap the
schema name in double quotes and escape any internal quotes) so the SQL targets
the correct schema when $schema contains mixed case or hyphens; ensure both the
"GRANT USAGE, CREATE ON SCHEMA" and the "GRANT USAGE ON ALL SEQUENCES IN SCHEMA"
statements are fixed and that any escaping logic for $schema is centralized or
reused.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5e212025-2a13-46cb-bea4-1917be98c853

📥 Commits

Reviewing files that changed from the base of the PR and between 9849119 and 37a4c7d.

📒 Files selected for processing (13)
  • src/Database/Concerns/ManagesPostgresUsers.php
  • src/Database/Concerns/ValidatesDatabaseParameters.php
  • src/Database/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/MySQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledMicrosoftSQLServerDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledPostgreSQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PermissionControlledPostgreSQLSchemaManager.php
  • src/Database/TenantDatabaseManagers/PostgreSQLDatabaseManager.php
  • src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php
  • src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php
  • src/Database/TenantDatabaseManagers/TenantDatabaseManager.php
  • tests/TenantDatabaseManagerTest.php

Comment thread src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/Bootstrappers/DatabaseTenancyBootstrapper.php (1)

54-62: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Exception scope is narrow — consider catching broader exceptions.

The try-catch only handles RuntimeException, so if Schema::hasTable() or the tenant query throws a QueryException (e.g., database connection issue), the connection remains on tenant without reverting. Since harden() is a security feature, you may want to catch \Throwable to ensure the connection always reverts on any failure.

♻️ Optional: broaden exception handling
         if (static::$harden) {
             try {
                 $this->harden($tenant);
-            } catch (RuntimeException $e) {
+            } catch (\Throwable $e) {
                 // Revert connection back to central
                 $this->revert();

                 throw $e;
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Bootstrappers/DatabaseTenancyBootstrapper.php` around lines 54 - 62, The
harden block currently only catches RuntimeException so failures like
QueryException or other throwables (e.g., from Schema::hasTable() or the tenant
query) will leave the connection switched to the tenant; update the catch to
handle all throwables (catch \Throwable $e) around the harden($tenant) call and
still call $this->revert() before re-throwing to guarantee the connection is
reverted on any failure while keeping the existing revert() and re-throw
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/Bootstrappers/DatabaseTenancyBootstrapper.php`:
- Around line 54-62: The harden block currently only catches RuntimeException so
failures like QueryException or other throwables (e.g., from Schema::hasTable()
or the tenant query) will leave the connection switched to the tenant; update
the catch to handle all throwables (catch \Throwable $e) around the
harden($tenant) call and still call $this->revert() before re-throwing to
guarantee the connection is reverted on any failure while keeping the existing
revert() and re-throw behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3d8be12b-69c5-4198-a261-b4f8c65e2452

📥 Commits

Reviewing files that changed from the base of the PR and between 405aaaf and fec170a.

📒 Files selected for processing (2)
  • src/Bootstrappers/DatabaseTenancyBootstrapper.php
  • tests/TenantDatabaseManagerTest.php

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/TenantDatabaseManagerTest.php`:
- Around line 709-712: The test currently hard-codes
'utf8mb4'/'utf8mb4_unicode_ci' for
expect(databaseCharset())/expect(databaseCollation()), which is brittle; instead
query the active DB connection for its defaults and assert against those values.
Replace the hard-coded expectations by retrieving the active connection's
default charset and collation (e.g. via a query for @@character_set_database and
@@collation_database on the current connection) and compare databaseCharset()
and databaseCollation() to those retrieved values so the test adapts to the
environment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 11193c91-4672-489d-9add-df2260da5e7e

📥 Commits

Reviewing files that changed from the base of the PR and between de91348 and 66ae88a.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.php

Comment thread tests/TenantDatabaseManagerTest.php
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/TenantDatabaseManagerTest.php`:
- Line 556: The negated instanceof check is parsed incorrectly; change the
condition in TenantDatabaseManagerTest where it reads if (! $manager instanceof
ManagesDatabaseUsers) to explicitly negate the whole instanceof expression (e.g.
wrap the instanceof in parentheses) so the branch correctly executes only when
$manager is not an instance of ManagesDatabaseUsers; update the condition
referencing $manager and ManagesDatabaseUsers accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 44fa1cd6-f9ba-480e-8d6b-ecd2bd4f5526

📥 Commits

Reviewing files that changed from the base of the PR and between de91348 and 0331875.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.php

Comment thread tests/TenantDatabaseManagerTest.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/TenantDatabaseManagerTest.php`:
- Around line 632-646: Extend the test for SQLiteDatabaseManager to also
exercise createDatabase() and deleteDatabase() in addition to databaseExists():
call createDatabase('valid-db_name.sqlite') and
deleteDatabase('valid-db_name.sqlite') and assert they do not throw
InvalidArgumentException for the valid filename, and call createDatabase('..'),
deleteDatabase('..'), createDatabase(''), and deleteDatabase('') and assert they
throw InvalidArgumentException for the invalid inputs; reference the
SQLiteDatabaseManager class and its methods databaseExists, createDatabase, and
deleteDatabase when adding these assertions so the mutating paths are covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3eb7f651-f899-4eee-9625-0ca636046907

📥 Commits

Reviewing files that changed from the base of the PR and between de91348 and bbd8f6f.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.php

Comment thread tests/TenantDatabaseManagerTest.php
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/TenantDatabaseManagerTest.php`:
- Around line 543-584: The test mutates database.connections.mysql.charset to an
array for the MySQL sub-check and does not restore it, so capture the original
config value (config('database.connections.mysql.charset')) before setting it to
[] in the $driver === 'mysql' branch, run DB::purge('mysql') as you already do,
then restore the original value with config([...]) and call DB::purge('mysql')
again to ensure subsequent assertions or tests use the original charset;
reference the MySQL branch in the test (the anonymous test function, the $driver
=== 'mysql' block, and the DB::purge('mysql') call) to locate where to add the
save/restore.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1a5b58c0-73cd-4645-bf75-070473e1a981

📥 Commits

Reviewing files that changed from the base of the PR and between de91348 and bbd8f6f.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.php

Comment thread tests/TenantDatabaseManagerTest.php
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/TenantDatabaseManagerTest.php`:
- Around line 614-621: Add a positive test case that verifies the
password-specific allowlist path: create a Tenant via Tenant::make with a
tenancy_db_password containing a character that the identifier validator would
reject but validatePassword() permits (e.g., an at-sign '@' or colon ':'), then
call $manager->createUser($tenant->database()) and assert it does NOT throw
InvalidArgumentException; this ensures the relaxed password validation in
validatePassword() is exercised separately from the stricter DB/user name
validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 67c05f4f-9675-43ce-aa34-23bff7398a68

📥 Commits

Reviewing files that changed from the base of the PR and between de91348 and 587f347.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.php

Comment thread tests/TenantDatabaseManagerTest.php
@lukinovec
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/TenantDatabaseManagerTest.php`:
- Around line 624-631: The test reuses the literal username 'valid_USERNAME'
when calling $manager->createUser on the Tenant created via Tenant::make, which
can collide with existing users in concurrent/rerun environments; update the
test to use a unique per-test username (e.g., generate one with Str::random()
and assign it to 'tenancy_db_username' on the Tenant) or explicitly remove the
created DB user after the assertion, ensuring the call to
$manager->createUser($tenantWithValidPassword->database()) uses the unique
username to avoid pre-existing user errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d34b5777-7da1-4c59-b886-0598f5953a37

📥 Commits

Reviewing files that changed from the base of the PR and between de91348 and 099a666.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.php

Comment thread tests/TenantDatabaseManagerTest.php
Comment thread tests/TenantDatabaseManagerTest.php
@lukinovec lukinovec marked this pull request as ready for review May 4, 2026 18:01
@lukinovec
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/TenantDatabaseManagerTest.php`:
- Around line 624-631: The test creates a real DB role via
$manager->createUser($tenantWithValidPassword->database()) but never removes it;
update the test to remove the created user by calling the manager's deleteUser
(or equivalent) with the same database/username after the assertion—either
immediately after the expect(...) assertion or in a finally block / test
teardown (afterEach) to ensure cleanup even on failure; reference the createUser
call and the deleteUser method on the same manager instance to locate where to
add the cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f24c5619-434f-42a1-a39e-184e8fc00591

📥 Commits

Reviewing files that changed from the base of the PR and between 099a666 and 649c802.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.php

Comment thread tests/TenantDatabaseManagerTest.php
@lukinovec lukinovec marked this pull request as draft May 4, 2026 21:59
@lukinovec lukinovec marked this pull request as ready for review May 5, 2026 06:36
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/TenantDatabaseManagerTest.php`:
- Around line 643-646: The comment in TenantDatabaseManagerTest referencing
validateParameter() and the
expect(...)->not()->toThrow(InvalidArgumentException::class) is misleading
because Pest will re-throw other exceptions (e.g., QueryException) causing the
test to fail; update the test to match intent: either change the assertion on
createUser($tenantWithNullDbParameters->database()) to expect the actual DB
exception (e.g., QueryException) or change the test to assert no exception by
preventing SQL execution (mocking/stubbing the DB call) or by making
createUser/validateParameter() explicitly throw InvalidArgumentException for
null params; reference validateParameter(), createUser(), and the
$tenantWithNullDbParameters setup when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3108bf56-af2b-4e5b-949f-c0b3cd2480ce

📥 Commits

Reviewing files that changed from the base of the PR and between 649c802 and 519c819.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.php

Comment thread tests/TenantDatabaseManagerTest.php
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
tests/TenantDatabaseManagerTest.php (1)

639-642: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Null-parameter assertion is likely a latent test failure — ->not()->toThrow(InvalidArgumentException::class) re-throws any other exception.

The inline comment explicitly says "an exception will be thrown, but not by validateParameter()". However, Pest's ->not()->toThrow(ExceptionClass) does not swallow non-matching exceptions — it re-throws them, causing the test to fail with that exception. If any ManagesDatabaseUsers backend throws a QueryException (or any other non-InvalidArgumentException) when createUser() is called with a null username, this test will fail with that propagated exception, not a clean assertion failure.

Options to resolve:

  1. If the intent is to verify that validateParameter() silently skips null and that createUser() completes without any exception, use ->not->toThrow() (no argument) to assert no exception at all; or
  2. If createUser() is expected to throw a different exception (e.g., QueryException), expect it explicitly, or remove this assertion and rely on unit tests of validateParameter() alone.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/TenantDatabaseManagerTest.php` around lines 639 - 642, The current
assertion expect(fn () =>
$manager->createUser($tenantWithNullDbParameters->database()))->not()->toThrow(InvalidArgumentException::class)
is unsafe because Pest will re-throw any other exception (e.g., a backend
QueryException) — fix by either 1) if you intended to assert that no exception
at all is thrown, change the assertion to ->not()->toThrow() so any exception
fails the test cleanly, or 2) if the backend is expected to throw a specific
different exception (e.g., QueryException from a ManagesDatabaseUsers
implementation), replace the assertion to
expect(...)->toThrow(QueryException::class) to explicitly assert that outcome;
locate this assertion near the validateParameter() / createUser() test and
update it accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@tests/TenantDatabaseManagerTest.php`:
- Around line 639-642: The current assertion expect(fn () =>
$manager->createUser($tenantWithNullDbParameters->database()))->not()->toThrow(InvalidArgumentException::class)
is unsafe because Pest will re-throw any other exception (e.g., a backend
QueryException) — fix by either 1) if you intended to assert that no exception
at all is thrown, change the assertion to ->not()->toThrow() so any exception
fails the test cleanly, or 2) if the backend is expected to throw a specific
different exception (e.g., QueryException from a ManagesDatabaseUsers
implementation), replace the assertion to
expect(...)->toThrow(QueryException::class) to explicitly assert that outcome;
locate this assertion near the validateParameter() / createUser() test and
update it accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 38d1c8e5-503d-46b9-a622-b722e76408c0

📥 Commits

Reviewing files that changed from the base of the PR and between 649c802 and d9ae274.

📒 Files selected for processing (1)
  • tests/TenantDatabaseManagerTest.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