Skip to content

feat: resource-scoped server access permissions for Viewer/Demo users#259

Open
kayvanaarssen wants to merge 5 commits intoDavid-Crty:mainfrom
kayvanaarssen:feature/resource-scoped-permissions
Open

feat: resource-scoped server access permissions for Viewer/Demo users#259
kayvanaarssen wants to merge 5 commits intoDavid-Crty:mainfrom
kayvanaarssen:feature/resource-scoped-permissions

Conversation

@kayvanaarssen
Copy link
Copy Markdown

@kayvanaarssen kayvanaarssen commented May 2, 2026

Context

This is a proof-of-concept implementation for #257.

The use case comes from hosting companies and MSPs running Databasement as a shared platform: they need to give individual clients (or teams) access to only their own database servers and databases — without promoting them to the Member role, which grants global access.

Currently the only options are:

  • Viewer — read-only, but sees everything
  • Member — can act, but sees everything
  • Admin — full control

There is no middle ground for "this user may only see Server A and only the prod database within it, and may download/restore but not delete."

This PR implements exactly that. It has been built and tested locally against the full test suite. It is presented as a starting point for the maintainers to validate, polish, and adapt to their standards before merging.


What this adds

user_server_accesses table (new migration)

A pivot table linking users to database servers with fine-grained control:

Column Type Description
user_id FK The user being granted access
database_server_id FK (ULID) The server they may access
allowed_databases JSON / null Specific database names, or null for all databases on the server
can_download bool May download snapshot files (default true)
can_backup bool May trigger manual backups (default false)
can_restore bool May restore snapshots (default false)
unique One grant per user/server pair

Scoping behaviour

The logic follows a simple principle: Admin and Member roles are completely unaffected. Only Viewer/Demo users with at least one grant become "scoped users". A Viewer with no grants retains the existing read-only-everything behaviour.

When a user is scoped, every listing query is filtered:

  • Server index — only shows granted servers
  • Snapshot/job index — shows only snapshots from granted servers, further filtered to allowed_databases when set
  • Restore modal — server dropdown and snapshot list both respect grants

Per-grant action permissions

Policy checks for backup, restore, and download now consult the grant for scoped users:

// DatabaseServerPolicy
public function backup(User $user, DatabaseServer $server): bool
{
    if ($user->isScopedUser()) {
        $access = $user->getServerAccess($server);
        return $access !== null && $access->can_backup;
    }
    return $user->isDemo() || $user->canPerformActions();
}

Server Access UI (admin side)

A new livewire:user.server-access component is embedded at the bottom of the user edit page. Admins can:

  • Grant access to a server (servers already granted are excluded from the dropdown)
  • Optionally restrict to specific databases — with live autocomplete from past snapshot history (no live DB connection required)
  • Toggle download / backup / restore permissions per grant
  • Revoke access with one click

Restore from jobs list

A restore button has been added to each completed backup job row (gated by @can('restore', $server)). Clicking it opens the restore modal with the snapshot pre-selected, skipping the snapshot-selection step.

Docker worker user fix

The queue worker was running as root, causing backup files to be created with 700 root permissions in /data/backups. The PHP process (running as application) could not read them, resulting in 404 on download. Fixed by adding user: "1000:1000" to the worker service in docker-compose.yml.


Files changed

New files:

  • database/migrations/2026_05_02_000001_create_user_server_accesses_table.php
  • database/migrations/2026_05_02_000002_add_can_backup_to_user_server_accesses_table.php
  • app/Models/UserServerAccess.php
  • database/factories/UserServerAccessFactory.php
  • app/Livewire/User/ServerAccess.php
  • resources/views/livewire/user/server-access.blade.php
  • tests/Feature/User/ServerAccessTest.php

Modified files:

  • app/Models/User.phpserverAccesses(), isScopedUser(), getAccessibleServerIds(), getServerAccess(), applyScopedSnapshotFilter()
  • app/Models/DatabaseServer.phpuserAccesses() relationship
  • app/Policies/DatabaseServerPolicy.phpview(), backup(), restore() honour grants for scoped users
  • app/Policies/SnapshotPolicy.phpview(), download() honour grants and allowed_databases for scoped users
  • app/Queries/DatabaseServerQuery.phpscopedUser parameter
  • app/Queries/SnapshotQuery.phpscopedUser parameter
  • app/Queries/BackupJobQuery.phpscopedUser parameter with per-database filtering
  • app/Livewire/DatabaseServer/Index.php — passes scopedUser to query
  • app/Livewire/DatabaseServer/RestoreModal.phpopen-restore-from-snapshot event listener; snapshot/server lists respect grants
  • app/Livewire/BackupJob/Index.phpconfirmRestoreFromJob(), passes scopedUser to query
  • resources/views/livewire/backup-job/index.blade.php — restore button per job row, includes restore modal
  • resources/views/livewire/user/edit.blade.php — embeds server-access component
  • docker-compose.yml — worker runs as user: "1000:1000"

Testing

All 914 existing tests pass. A dedicated test file covers:

  • Viewer without grants sees all servers/snapshots (unchanged behaviour)
  • Viewer with grants sees only granted servers and databases
  • Policy checks: view, backup, restore, download — scoped and unscoped
  • Database-level filtering (allowed_databases)
  • Admin UI: grant, revoke
  • BackupJob scoping
Tests: 914 passed (2711 assertions)

Notes for maintainers

This is a PoC and may need polishing before merge:

  • Migration timestamps use a fixed date — these should be adjusted to date('Y_m_d_His') format before merging to avoid ordering conflicts
  • The docker-compose.yml change (worker user) is a local dev fix — may warrant a separate PR
  • UserServerAccess has no soft-deletes; revoking a grant is a hard delete
  • The isScopedUser() method makes a DB query on every call — a cached version may be desirable for high-traffic installs
  • The dashboard (storage card, latest jobs widget) is not yet scoped for scoped users — snapshots/sizes shown there still reflect global state
  • Consider whether can_backup should default to true or remain false (currently false to be conservative)

Summary by CodeRabbit

  • New Features

    • Per-user server access management with configurable permissions (download, backup, restore) and UI on the user edit page
    • Restore-from-snapshot flow accessible from backup job rows and an enhanced restore modal
  • Improvements

    • Scoped visibility: restricted users now see only granted servers, snapshots, and backup jobs
    • Specialized restore handling for Redis/Valkey targets
  • Tests

    • End-to-end feature and policy tests covering access grants and scoped behavior

Closes David-Crty#257

Implements per-server and per-database access control for Viewer/Demo
users, allowing admins to grant clients access to only the servers and
databases they need — without promoting them to the Member role.

New features:
- user_server_accesses pivot table with per-server grants
- Optional allowed_databases JSON column (null = all databases on server)
- Per-grant action flags: can_download, can_backup, can_restore
- Server Access admin UI embedded in the user edit page
  - Live database autocomplete from past snapshot history
  - Add/remove individual database restrictions
- Scoped query filtering across server, snapshot, and job listings
- Restore button on backup job rows (opens modal pre-selecting snapshot)
- Worker container now runs as application user (fixes backup file permissions)

Architecture:
- Admin/Member roles are unaffected and always see everything
- Viewer with no grants: unchanged behaviour (read-only, all servers)
- Viewer with at least one grant: becomes a scoped user, visibility
  restricted to granted servers/databases across all index pages and policies
- User::isScopedUser() and applyScopedSnapshotFilter() are reusable
  entry points for future scoping requirements
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 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 a per-user server access system with grant records, migrations, factory, model relations and helpers, updates policies and query builders to enforce scoped visibility/permissions, introduces an admin Livewire UI to manage grants, and wires restore flows to respect scoped access and database-type restrictions.

Changes

User-Scoped Access Control & Restore Wiring

Layer / File(s) Summary
Data Model & Migrations
app/Models/UserServerAccess.php, database/migrations/2026_05_02_000001_create_user_server_accesses_table.php, database/migrations/2026_05_02_000002_add_can_backup_to_user_server_accesses_table.php, database/factories/UserServerAccessFactory.php
New UserServerAccess Eloquent model, migrations to create table and add can_backup, and factory with fluent helpers.
Model Relationships & Helpers
app/Models/User.php, app/Models/DatabaseServer.php
User adds serverAccesses() relation plus isScopedUser(), getAccessibleServerIds(), getServerAccess(), and applyScopedSnapshotFilter(). DatabaseServer adds userAccesses() relation and docblocks.
Authorization Policies
app/Policies/DatabaseServerPolicy.php, app/Policies/SnapshotPolicy.php
Policies updated to enforce scoped-user grants and permission flags: DatabaseServerPolicy::view/backup/restore and SnapshotPolicy::view/download now consult UserServerAccess (restore also denies agent-backed and REDIS types).
Query Builders
app/Queries/DatabaseServerQuery.php, app/Queries/BackupJobQuery.php, app/Queries/SnapshotQuery.php
Query builders accept optional ?User $scopedUser and, when provided, apply per-grant filters restricting servers, snapshots, backup/restore jobs and computed counts to accessible server IDs and allowed databases.
Livewire Components & Restore Flow
app/Livewire/User/ServerAccess.php, app/Livewire/DatabaseServer/RestoreModal.php, app/Livewire/BackupJob/Index.php, app/Livewire/DatabaseServer/Index.php
New ServerAccess Livewire component for granting/revoking access. RestoreModal gains openFromSnapshot() to pre-select snapshots (using scoped snapshot query). BackupJob/Index::confirmRestoreFromJob() added. DatabaseServer/Index::confirmRestore() handles Redis specially and defers agent/restore checks to policy. Components pass scoped user into queries.
Views & UI
resources/views/livewire/user/server-access.blade.php, resources/views/livewire/backup-job/index.blade.php, resources/views/livewire/user/edit.blade.php, resources/views/livewire/database-server/index.blade.php
New server-access UI and modal; backup-job rows now include a conditional Restore button and embed the restore modal; user edit view embeds the server-access component; server index restore button branches by database type.
Tests & Dev Config
tests/Feature/User/ServerAccessTest.php, docker-compose.yml
Feature tests covering scoped visibility, policy behavior, Livewire grant/revoke flows, and backup-job scoping. docker-compose.yml sets worker service user to 1000:1000.

Sequence Diagrams

sequenceDiagram
  actor Admin
  participant ServerAccess as ServerAccess Component
  participant DB as Database
  participant Policy as Authorization Policy

  Admin->>ServerAccess: openGrantModal()
  ServerAccess->>Policy: authorize('update', User)
  Policy-->>ServerAccess: authorized
  Admin->>ServerAccess: grantAccess(selectedServer, allowedDatabases, flags)
  ServerAccess->>DB: UserServerAccess::updateOrCreate(...)
  DB-->>ServerAccess: saved
  ServerAccess-->>Admin: success toast
Loading
sequenceDiagram
  actor ScopedUser as Scoped User
  participant BackupJob as BackupJob Index
  participant Modal as RestoreModal
  participant Query as SnapshotQuery
  participant Policy as SnapshotPolicy
  participant DB as Database

  ScopedUser->>BackupJob: click "Restore" on job
  BackupJob->>Modal: confirmRestoreFromJob(serverId, snapshotId)
  Modal->>Policy: authorize('restore', server)
  Policy-->>Modal: authorized/denied
  alt authorized
    Modal->>Query: buildFromParams(..., scopedUser)
    Query->>DB: fetch snapshot filtered by grants
    DB-->>Query: snapshot
    Query-->>Modal: snapshot found
    Modal-->>ScopedUser: show restore modal pre-selected
  else denied
    Modal-->>ScopedUser: authorization denied
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through grants with careful paws,
I mapped each server, noted laws.
Scoped users peek where granted lights glow,
Restores now check where rabbits go.
Permissions stitched, the garden's neat—hooray, access grows!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.64% 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 PR title 'feat: resource-scoped server access permissions for Viewer/Demo users' accurately summarizes the main change: implementing access control restrictions for non-admin users through per-server and per-database grants.
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 unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 6/8 reviews remaining, refill in 7 minutes and 32 seconds.

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

Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (1)
app/Queries/DatabaseServerQuery.php (1)

49-58: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Scope the server-level counts for database-restricted users too.

The row filter hides ungranted servers, but withCount('snapshots') and restores_count still aggregate all activity on a granted server. For a user restricted to specific allowed_databases, that can expose counts for other databases on the same server.

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

In `@app/Queries/DatabaseServerQuery.php` around lines 49 - 58, The counts are not
being filtered by a database-restricted user's allowed databases; update the
snapshot and restore counts to be scoped when $scopedUser is
database-restricted: replace withCount('snapshots') with withCount(['snapshots
as snapshots_count' => function (Builder $q) use ($scopedUser) { if
($scopedUser->isDatabaseRestricted()) $q->whereIn('database_id',
$scopedUser->getAllowedDatabaseIds()); }]) and change the restores_count
subquery (Restore::selectRaw(...)) to add the same database filter (e.g.,
->whereIn('database_id', $scopedUser->getAllowedDatabaseIds()) when
$scopedUser->isDatabaseRestricted()) inside the when($scopedUser, ...) scope so
both counts only include databases the user is allowed to see.
🧹 Nitpick comments (5)
docker-compose.yml (1)

31-31: Both app and worker services have hardcoded UID/GID; consider making them truly configurable for portability.

Line 31 hardcodes the worker to 1000:1000, but the app service (lines 18–19) also hardcodes these same values. While comments suggest they should be customizable, neither service currently uses environment variable interpolation. On hosts where the effective developer UID/GID differs, this can cause permission issues for backup/restore operations.

Consider updating both services to use interpolated values:

Suggested approach
worker:
-    user: "1000:1000"
+    user: "${UID:-1000}:${GID:-1000}"

app:
-      UID: 1000
-      GID: 1000
+      UID: ${UID:-1000}
+      GID: ${GID:-1000}

Then set UID and GID in a .env file or pass them via docker-compose -e.

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

In `@docker-compose.yml` at line 31, The docker-compose services "app" and
"worker" currently hardcode user: "1000:1000"; change both service definitions
to use environment-variable interpolation (e.g., user: "${UID}:${GID}") so the
UID/GID become configurable, and document that UID/GID should be provided via a
.env file or docker-compose environment (or passed on the CLI) so backup/restore
permission issues are avoided.
tests/Feature/User/ServerAccessTest.php (1)

94-98: ⚡ Quick win

Use the new UserServerAccessFactory states here.

These cases hand-write allowed_databases, can_restore, and can_download even though the factory now exposes withDatabases() / canRestore(), and can_download = true is already the default. Switching to the states will keep these tests aligned with the factory contract and make intent clearer.

Example
-    UserServerAccess::factory()->create([
-        'user_id' => $viewer->id,
-        'database_server_id' => $server->id,
-        'allowed_databases' => ['client_db'],
-        'can_download' => true,
-    ]);
+    UserServerAccess::factory()
+        ->for($viewer)
+        ->for($server, 'databaseServer')
+        ->withDatabases('client_db')
+        ->create();

As per coding guidelines, "When creating models for tests, use factories. Check if the factory has custom states before manually setting up the model."

Also applies to: 107-111, 123-127, 138-143, 154-159, 170-175

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

In `@tests/Feature/User/ServerAccessTest.php` around lines 94 - 98, Replace direct
attribute assignments on UserServerAccess::factory() with the factory's provided
states: use withDatabases([...]) to set allowed_databases and use canRestore()
(or a negated state if available, e.g., withoutRestore()/cannotRestore()) to set
can_restore instead of manually passing those attributes; omit setting
can_download because it's true by default. Update the instances at the shown
locations (the factory calls around the symbols UserServerAccess::factory() in
this test and the other occurrences listed) to call withDatabases(...) and the
appropriate canRestore/cannotRestore state so tests follow the factory contract
and intent.
app/Livewire/User/ServerAccess.php (3)

20-21: ⚡ Quick win

#[Locked] is redundant on Eloquent model properties.

Livewire already protects model properties from client-side ID tampering; keep #[Locked] for scalar props only.

Based on learnings, "In Livewire components, Eloquent model properties ... are automatically locked ... The #[Locked] attribute is only needed for scalar properties."

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

In `@app/Livewire/User/ServerAccess.php` around lines 20 - 21, The #[Locked]
attribute on the Eloquent model property should be removed: edit the Livewire
component class ServerAccess and delete the #[Locked] annotation from the public
User $user property (leave the property declaration intact); retain #[Locked]
only for scalar properties if needed elsewhere, since Livewire already
auto-locks Eloquent model properties against client-side ID tampering.

109-114: ⚡ Quick win

Move validation rules to #[Validate] attributes (or a Form object).

This component validates correctly, but the repository standard for Livewire is attribute/form-based rules with $this->validate() at execution time.

Suggested direction
+ use Livewire\Attributes\Validate;
...
- public string $selectedServerId = '';
+ #[Validate('required|exists:database_servers,id')]
+ public string $selectedServerId = '';

- public bool $canDownload = true;
+ #[Validate('boolean')]
+ public bool $canDownload = true;

As per coding guidelines, "In Livewire components, use #[Validate] attributes or Form objects for validation. Call $this->validate() before processing data."

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

In `@app/Livewire/User/ServerAccess.php` around lines 109 - 114, Move the inline
rules from the $this->validate() call into Livewire attribute-based validation
or a Form object: add #[Validate] attributes (or define a Form with rules) on
the component properties selectedServerId, canDownload, canBackup, and
canRestore in the ServerAccess Livewire component, then remove the hard-coded
array from the current validate() call and ensure $this->validate() is invoked
immediately before any processing (e.g., before saving or dispatching) so
runtime validation still occurs; reference the properties selectedServerId,
canDownload, canBackup, canRestore and the ServerAccess component when making
the change.

69-79: ⚡ Quick win

Reactive database search should be bounded for large snapshot tables.

This query is called during live typing and currently does a full distinct+order path without a cap. Add a small limit(...) (and pair with debounce in the input binding) to avoid heavy interactive queries.

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

In `@app/Livewire/User/ServerAccess.php` around lines 69 - 79, The Snapshot query
in the Livewire component (ServerAccess) is unbounded and can be expensive
during reactive typing; add a small limit (e.g., limit(50)) to the query chain
(place it after orderBy('database_name') and before pluck('database_name')) so
distinct/order/limit happen in SQL, and keep the existing filter/values/all()
logic; also ensure the Livewire input binding for $databaseSearch uses a
debounce (e.g., wire:model.debounce.300ms) to reduce query frequency while
typing.
🤖 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/Livewire/DatabaseServer/RestoreModal.php`:
- Around line 99-109: openFromSnapshot currently trusts the client-supplied
$snapshotId after only authorizing the target server; instead, load and
authorize the snapshot using the same scoped query path used later (the query
around Snapshot::findOrFail in the restore/select logic) before preselecting it
— e.g., fetch the snapshot via the target server's scoped relationship or the
controller's existing scoped query (matching the logic in the code around lines
264-270), verify it belongs to/is compatible with $this->targetServer, then set
selectedSnapshotId and call selectSnapshot; update openFromSnapshot to use that
scoped fetch (and fail/abort if not found or incompatible) rather than trusting
the raw ID.

In `@app/Livewire/User/ServerAccess.php`:
- Around line 151-155: availableServerOptions() currently excludes
already-granted servers via whereNotIn('id', $grantedIds), which prevents the UI
from selecting an existing server when editing a grant; change
availableServerOptions (and its callers like grantAccess()) to accept an
optional $currentServerId (nullable) and when present remove that id from
$grantedIds before applying whereNotIn so the server tied to the grant being
edited remains selectable (i.e., only exclude other granted ids); ensure
grantAccess() passes the current grant's database_server_id when editing so
updateOrCreate() can update existing grants.

In `@app/Policies/DatabaseServerPolicy.php`:
- Around line 91-99: The policy's restore(User $user, DatabaseServer
$databaseServer) currently only checks role/grant flags and must also enforce
the same agent/Redis constraints the UI uses; update restore to refuse restores
for servers that the UI blocks by calling the same predicate(s) the
UI/controller uses (e.g. reuse or call the existing logic in
DatabaseServerController::restore or a DatabaseServer model method such as
isRestorable()/isAgentManaged()/isRedisOnly() that mirrors the UI checks) in
addition to the existing scoped-user and demo/canPerformActions checks so API
restores are prevented for non-restorable servers.

In `@app/Queries/BackupJobQuery.php`:
- Around line 97-109: The restore branch only filters by target_server_id and
ignores $access->allowed_databases, leaking jobs; update the
orWhereHas('restore', ...) closure in BackupJobQuery to also apply the allowlist
when $access->allowed_databases !== null (e.g. add a whereIn on the restore's
database column using $access->allowed_databases), keeping the existing
target_server_id check and preserving behavior when allowed_databases is null;
reference the scopedUser->serverAccesses(), the snapshot and restore relations,
and the $access->allowed_databases property when making the change.

In `@resources/views/livewire/backup-job/index.blade.php`:
- Around line 146-150: The x-button component's tooltip attribute is using Blade
interpolation which double-encodes translated text; update the attribute on the
<x-button> that calls confirmRestoreFromJob (the button with icon="o-arrow-path"
and wire:click="confirmRestoreFromJob(...)") to use dynamic binding by changing
tooltip="{{ __('Restore') }}" to :tooltip="__('Restore')" so the translated
string is passed as a raw attribute.

In `@resources/views/livewire/user/server-access.blade.php`:
- Line 17: Replace Blade interpolation of translated strings inside component
attributes with dynamic :attr bindings to avoid double-encoding; e.g., change
label="{{ __('Grant Access') }}" to use :label="__('Grant Access')", and apply
the same change for all other translated attributes in this view
(titles/labels/placeholders such as "Revoke Access", "Search", "Save", "Cancel",
and the other occurrences noted). Update each component attribute in
server-access.blade.php that currently uses {{ __('...') }} so it uses the
colon-prefixed dynamic syntax :attribute="__('...')" while preserving the same
translation keys/function calls and existing attribute names.

---

Outside diff comments:
In `@app/Queries/DatabaseServerQuery.php`:
- Around line 49-58: The counts are not being filtered by a database-restricted
user's allowed databases; update the snapshot and restore counts to be scoped
when $scopedUser is database-restricted: replace withCount('snapshots') with
withCount(['snapshots as snapshots_count' => function (Builder $q) use
($scopedUser) { if ($scopedUser->isDatabaseRestricted())
$q->whereIn('database_id', $scopedUser->getAllowedDatabaseIds()); }]) and change
the restores_count subquery (Restore::selectRaw(...)) to add the same database
filter (e.g., ->whereIn('database_id', $scopedUser->getAllowedDatabaseIds())
when $scopedUser->isDatabaseRestricted()) inside the when($scopedUser, ...)
scope so both counts only include databases the user is allowed to see.

---

Nitpick comments:
In `@app/Livewire/User/ServerAccess.php`:
- Around line 20-21: The #[Locked] attribute on the Eloquent model property
should be removed: edit the Livewire component class ServerAccess and delete the
#[Locked] annotation from the public User $user property (leave the property
declaration intact); retain #[Locked] only for scalar properties if needed
elsewhere, since Livewire already auto-locks Eloquent model properties against
client-side ID tampering.
- Around line 109-114: Move the inline rules from the $this->validate() call
into Livewire attribute-based validation or a Form object: add #[Validate]
attributes (or define a Form with rules) on the component properties
selectedServerId, canDownload, canBackup, and canRestore in the ServerAccess
Livewire component, then remove the hard-coded array from the current validate()
call and ensure $this->validate() is invoked immediately before any processing
(e.g., before saving or dispatching) so runtime validation still occurs;
reference the properties selectedServerId, canDownload, canBackup, canRestore
and the ServerAccess component when making the change.
- Around line 69-79: The Snapshot query in the Livewire component (ServerAccess)
is unbounded and can be expensive during reactive typing; add a small limit
(e.g., limit(50)) to the query chain (place it after orderBy('database_name')
and before pluck('database_name')) so distinct/order/limit happen in SQL, and
keep the existing filter/values/all() logic; also ensure the Livewire input
binding for $databaseSearch uses a debounce (e.g., wire:model.debounce.300ms) to
reduce query frequency while typing.

In `@docker-compose.yml`:
- Line 31: The docker-compose services "app" and "worker" currently hardcode
user: "1000:1000"; change both service definitions to use environment-variable
interpolation (e.g., user: "${UID}:${GID}") so the UID/GID become configurable,
and document that UID/GID should be provided via a .env file or docker-compose
environment (or passed on the CLI) so backup/restore permission issues are
avoided.

In `@tests/Feature/User/ServerAccessTest.php`:
- Around line 94-98: Replace direct attribute assignments on
UserServerAccess::factory() with the factory's provided states: use
withDatabases([...]) to set allowed_databases and use canRestore() (or a negated
state if available, e.g., withoutRestore()/cannotRestore()) to set can_restore
instead of manually passing those attributes; omit setting can_download because
it's true by default. Update the instances at the shown locations (the factory
calls around the symbols UserServerAccess::factory() in this test and the other
occurrences listed) to call withDatabases(...) and the appropriate
canRestore/cannotRestore state so tests follow the factory contract and intent.
🪄 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: CHILL

Plan: Pro

Run ID: eb999027-d924-4ff1-9350-3e83922efe56

📥 Commits

Reviewing files that changed from the base of the PR and between d07c66d and b6f5560.

📒 Files selected for processing (20)
  • app/Livewire/BackupJob/Index.php
  • app/Livewire/DatabaseServer/Index.php
  • app/Livewire/DatabaseServer/RestoreModal.php
  • app/Livewire/User/ServerAccess.php
  • app/Models/DatabaseServer.php
  • app/Models/User.php
  • app/Models/UserServerAccess.php
  • app/Policies/DatabaseServerPolicy.php
  • app/Policies/SnapshotPolicy.php
  • app/Queries/BackupJobQuery.php
  • app/Queries/DatabaseServerQuery.php
  • app/Queries/SnapshotQuery.php
  • database/factories/UserServerAccessFactory.php
  • database/migrations/2026_05_02_000001_create_user_server_accesses_table.php
  • database/migrations/2026_05_02_000002_add_can_backup_to_user_server_accesses_table.php
  • docker-compose.yml
  • resources/views/livewire/backup-job/index.blade.php
  • resources/views/livewire/user/edit.blade.php
  • resources/views/livewire/user/server-access.blade.php
  • tests/Feature/User/ServerAccessTest.php
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.php: In PHP files, always use curly braces for control structures, even for single-line bodies.
Use PHP 8 constructor property promotion: public function __construct(public GitHub $github) { }. Do not leave empty zero-parameter __construct() methods unless the constructor is private.
Use explicit return type declarations and type hints for all method parameters in PHP: function isAccessible(User $user, ?string $path = null): bool
Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
Use array shape type definitions in PHPDoc blocks for PHP.

Files:

  • app/Livewire/DatabaseServer/Index.php
  • database/migrations/2026_05_02_000002_add_can_backup_to_user_server_accesses_table.php
  • resources/views/livewire/user/edit.blade.php
  • database/migrations/2026_05_02_000001_create_user_server_accesses_table.php
  • app/Models/UserServerAccess.php
  • app/Policies/DatabaseServerPolicy.php
  • app/Queries/DatabaseServerQuery.php
  • database/factories/UserServerAccessFactory.php
  • app/Livewire/DatabaseServer/RestoreModal.php
  • app/Queries/BackupJobQuery.php
  • app/Queries/SnapshotQuery.php
  • resources/views/livewire/backup-job/index.blade.php
  • app/Policies/SnapshotPolicy.php
  • app/Livewire/BackupJob/Index.php
  • resources/views/livewire/user/server-access.blade.php
  • app/Livewire/User/ServerAccess.php
  • app/Models/User.php
  • tests/Feature/User/ServerAccessTest.php
  • app/Models/DatabaseServer.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/Livewire/**/*.php: In Livewire components, use #[Validate] attributes or Form objects for validation. Call $this->validate() before processing data.
In Livewire components, use Session::flash() for one-time messages and show them via @if (session('success')) in Blade templates.
In Livewire components, return $this->redirect() with navigate: true for SPA-like navigation.
In Mary UI modals, add a boolean property to the component class and use wire:model in the Blade template.
Keep state server-side in Livewire components so the UI reflects it. Validate and authorize in actions as you would in HTTP requests.

Files:

  • app/Livewire/DatabaseServer/Index.php
  • app/Livewire/DatabaseServer/RestoreModal.php
  • app/Livewire/BackupJob/Index.php
  • app/Livewire/User/ServerAccess.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{php,blade.php}: Always use named routes and the route() function when generating links to other pages.
Translations should use __('...') helper function and be stored in JSON translation files in lang/{locale}.json. Keep technical terms like 'Backup', 'Restore', 'Snapshot(s)' in English across all locales.

Files:

  • app/Livewire/DatabaseServer/Index.php
  • database/migrations/2026_05_02_000002_add_can_backup_to_user_server_accesses_table.php
  • resources/views/livewire/user/edit.blade.php
  • database/migrations/2026_05_02_000001_create_user_server_accesses_table.php
  • app/Models/UserServerAccess.php
  • app/Policies/DatabaseServerPolicy.php
  • app/Queries/DatabaseServerQuery.php
  • database/factories/UserServerAccessFactory.php
  • app/Livewire/DatabaseServer/RestoreModal.php
  • app/Queries/BackupJobQuery.php
  • app/Queries/SnapshotQuery.php
  • resources/views/livewire/backup-job/index.blade.php
  • app/Policies/SnapshotPolicy.php
  • app/Livewire/BackupJob/Index.php
  • resources/views/livewire/user/server-access.blade.php
  • app/Livewire/User/ServerAccess.php
  • app/Models/User.php
  • tests/Feature/User/ServerAccessTest.php
  • app/Models/DatabaseServer.php
resources/views/livewire/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

All Livewire component blade files should contain only view markup; all PHP logic must be in the component class.

Files:

  • resources/views/livewire/user/edit.blade.php
  • resources/views/livewire/backup-job/index.blade.php
  • resources/views/livewire/user/server-access.blade.php
resources/views/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

resources/views/**/*.blade.php: All UI components in Mary UI should be prefixed with x- (e.g., <x-button>, <x-input>, <x-card>) and use Heroicons for icons (e.g., icon="o-user" for outline, icon="s-user" for solid).
In Mary UI select components, use the :options prop with array format [['id' => 'value', 'name' => 'Label']].
In Mary UI alerts, use class="alert-success", class="alert-error", etc. (not variant prop).
Use Alpine.js for client-side interactions in Livewire instead of JavaScript frameworks.
In Blade templates, use :attr binding (dynamic syntax) instead of {{ }} interpolation when passing translated strings to component attributes to avoid double-encoding special characters.

Files:

  • resources/views/livewire/user/edit.blade.php
  • resources/views/livewire/backup-job/index.blade.php
  • resources/views/livewire/user/server-access.blade.php
tests/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.php: When creating models for tests, use factories. Check if the factory has custom states before manually setting up the model.
Use faker methods such as $this->faker->word() or fake()->randomDigit() when generating test data. Follow existing conventions on whether to use $this->faker or fake().
Create Pest tests using php artisan make:test --pest {name} without including the test suite directory. Use php artisan make:test --pest SomeFeatureTest instead of php artisan make:test --pest Feature/SomeFeatureTest.

Files:

  • tests/Feature/User/ServerAccessTest.php
🧠 Learnings (7)
📚 Learning: 2026-02-18T09:45:52.485Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 116
File: app/Livewire/DatabaseServer/ConnectionStatus.php:18-18
Timestamp: 2026-02-18T09:45:52.485Z
Learning: In Livewire components, Eloquent model properties (e.g., public DatabaseServer $server) are automatically locked by the framework to prevent client-side ID tampering. The #[Locked] attribute is only needed for scalar properties (int, string, bool, etc.) that require protection from client-side mutation. Apply this guidance to all Livewire PHP components; use #[Locked] only on primitive properties that you want to shield from client manipulation, and rely on automatic locking for Eloquent model properties.

Applied to files:

  • app/Livewire/DatabaseServer/Index.php
  • app/Livewire/DatabaseServer/RestoreModal.php
  • app/Livewire/BackupJob/Index.php
  • app/Livewire/User/ServerAccess.php
📚 Learning: 2026-02-25T10:48:17.811Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 132
File: app/Console/Commands/RecoverAgentLeasesCommand.php:44-48
Timestamp: 2026-02-25T10:48:17.811Z
Learning: When reviewing PHP code, especially with foreign keys that use cascadeOnDelete and are non-nullable, assume child relations exist at runtime (the database will delete children when the parent is deleted). Do not rely on null-safe operators for these relations, as PHPStan already models them as non-null. This guideline applies broadly to PHP files that define models with foreign keys using cascade delete; verify there are no unnecessary null checks or optional chaining on such relations.

Applied to files:

  • app/Livewire/DatabaseServer/Index.php
  • database/migrations/2026_05_02_000002_add_can_backup_to_user_server_accesses_table.php
  • resources/views/livewire/user/edit.blade.php
  • database/migrations/2026_05_02_000001_create_user_server_accesses_table.php
  • app/Models/UserServerAccess.php
  • app/Policies/DatabaseServerPolicy.php
  • app/Queries/DatabaseServerQuery.php
  • database/factories/UserServerAccessFactory.php
  • app/Livewire/DatabaseServer/RestoreModal.php
  • app/Queries/BackupJobQuery.php
  • app/Queries/SnapshotQuery.php
  • resources/views/livewire/backup-job/index.blade.php
  • app/Policies/SnapshotPolicy.php
  • app/Livewire/BackupJob/Index.php
  • resources/views/livewire/user/server-access.blade.php
  • app/Livewire/User/ServerAccess.php
  • app/Models/User.php
  • tests/Feature/User/ServerAccessTest.php
  • app/Models/DatabaseServer.php
📚 Learning: 2026-04-10T08:46:55.182Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 205
File: database/migrations/2026_04_09_000001_add_notification_channels.php:143-156
Timestamp: 2026-04-10T08:46:55.182Z
Learning: In David-Crty/databasement, treat Laravel migrations as forward-only for production. For migrations in database/migrations/*.php, it is acceptable to delete or transform legacy data in the migration’s up() (e.g., removing app_configs rows) without restoring it in down(), even if down() cannot fully reverse the state. During code review, do not flag missing rollback restoration of migrated/deleted data as a production risk for this repo’s migrations.

Applied to files:

  • database/migrations/2026_05_02_000002_add_can_backup_to_user_server_accesses_table.php
  • database/migrations/2026_05_02_000001_create_user_server_accesses_table.php
📚 Learning: 2026-01-30T22:27:46.107Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 61
File: resources/views/livewire/volume/connectors/s3-config.blade.php:1-13
Timestamp: 2026-01-30T22:27:46.107Z
Learning: In Blade template files (any .blade.php) within the databasement project, allow using alert-info for informational content inside <x-alert> components. The guideline that permits alert-success and alert-error does not exclude using alert-info for informational purposes. Apply this consistently to all Blade components that render alerts; ensure semantic usage and accessibility.

Applied to files:

  • resources/views/livewire/user/edit.blade.php
  • resources/views/livewire/backup-job/index.blade.php
  • resources/views/livewire/user/server-access.blade.php
📚 Learning: 2026-02-06T10:34:43.585Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 75
File: resources/views/livewire/backup-job/_filters.blade.php:36-40
Timestamp: 2026-02-06T10:34:43.585Z
Learning: In Blade template files, when creating compact inline filter controls, prefer using native <input type="checkbox"> elements with daisyUI classes (e.g., checkbox checkbox-warning checkbox-xs) over the Mary UI <x-checkbox> component. The <x-checkbox> component adds wrapper markup (e.g., <div><fieldset><label> with gap-3) that can break tight inline flex layouts. Use the native input approach for compact inline controls, but reserve <x-checkbox> for form fields that require labels, hints, and errors.

Applied to files:

  • resources/views/livewire/user/edit.blade.php
  • resources/views/livewire/backup-job/index.blade.php
  • resources/views/livewire/user/server-access.blade.php
📚 Learning: 2026-02-13T11:05:37.072Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 105
File: tests/Feature/Services/Backup/Databases/SqliteDatabaseTest.php:8-11
Timestamp: 2026-02-13T11:05:37.072Z
Learning: Adopt a global afterEach hook in tests/Pest.php (or equivalent Pest bootstrap) to clean up temporary directories created during tests. Specifically handle temp dirs named with the prefixes sqlite-db-test-*, backup-task-test-*, restore-task-test-*, and volume-test-*, so individual test files don’t need their own cleanup logic. This applies to all PHP test files under the tests directory.

Applied to files:

  • tests/Feature/User/ServerAccessTest.php
📚 Learning: 2026-04-09T13:59:25.873Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 205
File: tests/Feature/ConfigurationTest.php:165-176
Timestamp: 2026-04-09T13:59:25.873Z
Learning: In this repository, `Notification::fake()` is already performed globally in the base test class (`tests/TestCase.php`) within `setUp()` before each test. When reviewing individual test files under `tests/`, do not flag missing `Notification::fake()` calls, since they are handled by the shared base test setup.

Applied to files:

  • tests/Feature/User/ServerAccessTest.php

Comment thread app/Livewire/DatabaseServer/RestoreModal.php Outdated
Comment thread app/Livewire/User/ServerAccess.php
Comment thread app/Policies/DatabaseServerPolicy.php
Comment thread app/Queries/BackupJobQuery.php
Comment thread resources/views/livewire/backup-job/index.blade.php
Comment thread resources/views/livewire/user/server-access.blade.php Outdated
@David-Crty
Copy link
Copy Markdown
Owner

Hey @kayvanaarssen thanks for your suggestion and PR, one thing that I was thinking in order to simplify this would be to create a new "global" model named "Organization".
A user could be member of more than one organization and admin can manage all orgs.
In the main menu user could switch orgs using a select input.

Current models would be impacted and attach to an organization:

  • databaserver
  • volume

Would this organzation feature match your needs ?

Not sure if I made my self clear about this approach.

@kayvanaarssen
Copy link
Copy Markdown
Author

kayvanaarssen commented May 2, 2026

Hey @kayvanaarssen thanks for your suggestion and PR, one thing that I was thinking in order to simplify this would be to create a new "global" model named "Organization". A user could be member of more than one organization and admin can manage all orgs. In the main menu user could switch orgs using a select input.

Current models would be impacted and attach to an organization:

  • databaserver
  • volume

Would this organzation feature match your needs ?

Not sure if I made my self clear about this approach.


TL;DR: Built a PoC showing it's feasible. Need a backup tool that lets devs access/restore specific client DBs — current mysqldump + restic + S3 setup doesn't support that. Found a project that fits.
For access control, want a mix of org-level and per-user permissions: orgs get full server or selected DB access, but single users should be assignable to just one DB without needing a full org setup. Still need to fix views so users only see the DBs they have access to, not everything on the server.


Yeah that's maybe even better - i've just created this with a little help of Ai as a PoC to see if it could be done within the current scope and it is. We are facing questions from time to time from devs working on clients sites if they can make extra backups / restore or download them.
Our current setup - mysqldump -> Sync with restic hourly to our S3 is not setup to facilitate that.

So came across your project and its exactly what we are looking for.

Back to your reply.
Yes organisations would be the option as well but if we for example maybe only need to give 1 user access to a database setting up a complete organisation is maybe a bit to much. Like adding a users -> assigning a server and one DB is enough sometimes.
So a good mix of both would be amazing!

For origanisation i would still opt for;

  • Access to server (all DB's)
  • Access to Server selected DB's

And then with the options - got some screenshots here.
Thing that i did not fix yet - is the views like - a user may see to much info now - like how many DB's are on the server / backed up while they only need to see one.

Happy to work on this feature with you to facilitate the extra options
Screenshots below;

2026-05-02_09-52-42 2026-05-02_09-52-31

@David-Crty
Copy link
Copy Markdown
Owner

We would need to this in 2 separated steps and PR because thouse changes have a big impact

  1. Adding the organization feature
  2. Check how we can restrict database server to some users.

Feel free to explore the options I will check this deeper next week.

@kayvanaarssen
Copy link
Copy Markdown
Author

We would need to this in 2 separated steps and PR because thouse changes have a big impact

  1. Adding the organization feature
  2. Check how we can restrict database server to some users.

Feel free to explore the options I will check this deeper next week.

Amazing! Thanks for the positive feedback on this!

- Verify snapshot accessibility via scoped SnapshotQuery in
  RestoreModal::openFromSnapshot() before pre-selecting, preventing
  a scoped user from opening a snapshot they cannot access by
  supplying an arbitrary ULID through the Livewire event

- Apply allowed_databases filter to restore jobs in BackupJobQuery
  so scoped users cannot see restore jobs for databases outside
  their grants

- Scope snapshot and restore counts in DatabaseServerQuery to the
  union of allowed databases for scoped users, preventing count
  leakage on the server index

- Support editing existing grants in ServerAccess: openGrantModal()
  now accepts an optional access ID, pre-fills the form, and keeps
  the server selectable by excluding it from the already-granted
  exclusion list; pencil edit button added alongside revoke

- Use :tooltip binding (dynamic syntax) on the restore button in
  the jobs table to avoid HTML double-encoding
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@app/Queries/DatabaseServerQuery.php`:
- Around line 56-79: The bug is that $allAllowedDbs flattens grants across all
servers and then reuses that global list for every database_servers row; instead
build per-server correlated filters so each database_server uses only its own
serverAccess row(s). Replace the flattened $allAllowedDbs logic with correlated
subqueries/conditions: for snapshots use withCount(['snapshots' => fn(Builder
$q) => $q->whereExists(...)]) and for restores replace the
Restore::selectRaw(...) with a correlated subselect that joins/filters
server_accesses for the current database_server (match database_servers.id to
server_accesses.database_server_id and scoped user id), and enforce
(server_accesses.allowed_databases IS NULL) OR (the snapshot/schema name is
contained in server_accesses.allowed_databases) so unrestricted grants count all
DBs while restricted grants only count DB names for that specific server; keep
using serverAccesses(), allowed_databases, snapshots, and Restore as the
reference points when implementing these correlated filters.
- Around line 51-55: The when() is applied for any non-null User and will hide
all servers when getAccessibleServerIds() is empty; change the predicate so the
whereIn('id', $scopedUser->getAccessibleServerIds()) is only applied for
actually scoped users (e.g. replace the condition with $scopedUser !== null &&
$scopedUser->isScoped() or the project’s equivalent scoped-user predicate),
keeping the closure using Builder and getAccessibleServerIds(); if your
predicate name differs, call that predicate method inside the when() first
argument so Admin/Member and un-granted Viewers keep the global listing.
🪄 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: CHILL

Plan: Pro

Run ID: d566a11c-3f7e-44cc-bab3-bd69c488055c

📥 Commits

Reviewing files that changed from the base of the PR and between b6f5560 and e03f573.

📒 Files selected for processing (6)
  • app/Livewire/DatabaseServer/RestoreModal.php
  • app/Livewire/User/ServerAccess.php
  • app/Queries/BackupJobQuery.php
  • app/Queries/DatabaseServerQuery.php
  • resources/views/livewire/backup-job/index.blade.php
  • resources/views/livewire/user/server-access.blade.php
✅ Files skipped from review due to trivial changes (1)
  • resources/views/livewire/user/server-access.blade.php
🚧 Files skipped from review as they are similar to previous changes (4)
  • resources/views/livewire/backup-job/index.blade.php
  • app/Livewire/DatabaseServer/RestoreModal.php
  • app/Livewire/User/ServerAccess.php
  • app/Queries/BackupJobQuery.php
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.php: In PHP files, always use curly braces for control structures, even for single-line bodies.
Use PHP 8 constructor property promotion: public function __construct(public GitHub $github) { }. Do not leave empty zero-parameter __construct() methods unless the constructor is private.
Use explicit return type declarations and type hints for all method parameters in PHP: function isAccessible(User $user, ?string $path = null): bool
Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
Use array shape type definitions in PHPDoc blocks for PHP.

Files:

  • app/Queries/DatabaseServerQuery.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{php,blade.php}: Always use named routes and the route() function when generating links to other pages.
Translations should use __('...') helper function and be stored in JSON translation files in lang/{locale}.json. Keep technical terms like 'Backup', 'Restore', 'Snapshot(s)' in English across all locales.

Files:

  • app/Queries/DatabaseServerQuery.php
🧠 Learnings (1)
📚 Learning: 2026-02-25T10:48:17.811Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 132
File: app/Console/Commands/RecoverAgentLeasesCommand.php:44-48
Timestamp: 2026-02-25T10:48:17.811Z
Learning: When reviewing PHP code, especially with foreign keys that use cascadeOnDelete and are non-nullable, assume child relations exist at runtime (the database will delete children when the parent is deleted). Do not rely on null-safe operators for these relations, as PHPStan already models them as non-null. This guideline applies broadly to PHP files that define models with foreign keys using cascade delete; verify there are no unnecessary null checks or optional chaining on such relations.

Applied to files:

  • app/Queries/DatabaseServerQuery.php

Comment thread app/Queries/DatabaseServerQuery.php
Comment thread app/Queries/DatabaseServerQuery.php Outdated
- Block agent-backed server restores at the policy level; add Redis and
  agent guards to the API restore endpoint so automated restore cannot be
  triggered via API for unsupported server types (Redis manual-instructions
  UX in the UI is preserved since the policy now only blocks agents)

- Gate the scoped-user query branch in DatabaseServerQuery on
  isScopedUser() so Admin/Member users passed as scopedUser never receive
  a restricted listing; replace the flat allowed-databases union with
  per-server correlated OR-WHERE conditions so snapshot and restore counts
  are filtered by each server's own grant rather than a merged global list

- Convert {{ __('...') }} interpolation to :attr="__('...')" dynamic
  bindings throughout server-access.blade.php to avoid double-encoding of
  translated component attributes
Copy link
Copy Markdown

@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 `@app/Http/Controllers/Api/V1/DatabaseServerController.php`:
- Around line 199-205: The controller currently rejects Redis/Valkey restores in
DatabaseServerController (restore path) but DatabaseServerPolicy::restore()
still permits them, causing authorize('restore') to succeed then fail with 422;
update DatabaseServerPolicy::restore() to include the same check (deny when
$databaseServer->database_type === DatabaseType::REDIS or Valkey-equivalent) so
authorization and controller behavior match, and then remove the duplicate
Redis/Valkey check from DatabaseServerController to keep logic centralized and
consistent.
🪄 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: CHILL

Plan: Pro

Run ID: 4799d170-cbf4-4f27-a0ae-e8e8f26addea

📥 Commits

Reviewing files that changed from the base of the PR and between e03f573 and 2fdbf85.

📒 Files selected for processing (4)
  • app/Http/Controllers/Api/V1/DatabaseServerController.php
  • app/Policies/DatabaseServerPolicy.php
  • app/Queries/DatabaseServerQuery.php
  • resources/views/livewire/user/server-access.blade.php
✅ Files skipped from review due to trivial changes (1)
  • resources/views/livewire/user/server-access.blade.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Policies/DatabaseServerPolicy.php
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.php: In PHP files, always use curly braces for control structures, even for single-line bodies.
Use PHP 8 constructor property promotion: public function __construct(public GitHub $github) { }. Do not leave empty zero-parameter __construct() methods unless the constructor is private.
Use explicit return type declarations and type hints for all method parameters in PHP: function isAccessible(User $user, ?string $path = null): bool
Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
Use array shape type definitions in PHPDoc blocks for PHP.

Files:

  • app/Http/Controllers/Api/V1/DatabaseServerController.php
  • app/Queries/DatabaseServerQuery.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{php,blade.php}: Always use named routes and the route() function when generating links to other pages.
Translations should use __('...') helper function and be stored in JSON translation files in lang/{locale}.json. Keep technical terms like 'Backup', 'Restore', 'Snapshot(s)' in English across all locales.

Files:

  • app/Http/Controllers/Api/V1/DatabaseServerController.php
  • app/Queries/DatabaseServerQuery.php
🧠 Learnings (2)
📚 Learning: 2026-02-25T10:48:17.811Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 132
File: app/Console/Commands/RecoverAgentLeasesCommand.php:44-48
Timestamp: 2026-02-25T10:48:17.811Z
Learning: When reviewing PHP code, especially with foreign keys that use cascadeOnDelete and are non-nullable, assume child relations exist at runtime (the database will delete children when the parent is deleted). Do not rely on null-safe operators for these relations, as PHPStan already models them as non-null. This guideline applies broadly to PHP files that define models with foreign keys using cascade delete; verify there are no unnecessary null checks or optional chaining on such relations.

Applied to files:

  • app/Http/Controllers/Api/V1/DatabaseServerController.php
  • app/Queries/DatabaseServerQuery.php
📚 Learning: 2026-03-23T11:45:15.846Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 178
File: app/Http/Controllers/Api/V1/VolumeController.php:116-120
Timestamp: 2026-03-23T11:45:15.846Z
Learning: For the API v1 `testConnection` endpoints in this repo (e.g., `VolumeController::testConnection`, `DatabaseServerController::testConnection`), keep using `$this->authorize('view', $model)` for authorization. This is an intentional maintainer design choice; do not flag it as a bug or as insufficiently strict authorization in future reviews for these `testConnection` actions.

Applied to files:

  • app/Http/Controllers/Api/V1/DatabaseServerController.php
🔇 Additional comments (4)
app/Queries/DatabaseServerQuery.php (4)

51-55: Scoped gating now correctly protects Admin/Member and ungranted Viewer behavior.

Good fix on Line 52: the branch only applies access restriction when isScopedUser() is true, which matches the PR contract.


61-72: Snapshot counting is now properly grant-correlated per server.

The nested OR structure on Line 62-Line 71 keeps snapshot counting tied to each explicit access grant instead of using a global merged DB list.


74-86: restores_count correlation is aligned with per-server access rules.

Nice update: Line 75 plus the scoped OR branches on Line 76-Line 85 keep restore counts constrained to server grant + optional schema allowlist.


88-93: Fallback branch preserves existing global behavior for non-scoped users.

Line 88-Line 93 cleanly retains the previous unscoped counting behavior, which keeps this change isolated to scoped users.

Comment thread app/Http/Controllers/Api/V1/DatabaseServerController.php Outdated
Move the Redis/Valkey automated-restore restriction from the API
controller into DatabaseServerPolicy::restore() alongside the existing
agent check, so authorization is the single source of truth for both
the UI and the API.

The Redis manual-instructions flow is preserved by checking for Redis
before the authorize() call in Index::confirmRestore(), since showing
documentation does not dispatch a restore job. The blade button is
similarly split so Redis servers show a "View Restore Instructions"
button outside the @can('restore') gate, while all other servers remain
policy-gated as before. The now-redundant 422 guards in the API
controller are removed.
Copy link
Copy Markdown

@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 `@app/Livewire/DatabaseServer/Index.php`:
- Around line 115-126: The Redis branch currently returns before authorization
and before setting component state, allowing unauthorized access and leaving
$this->restoreId stale; call $this->authorize('restore', $server) first, then
set $this->restoreId = $id, and only after those do the DatabaseType::REDIS
check and set $this->showRedisRestoreModal = true with an early return;
reference the authorize('restore', $server) call, $this->restoreId assignment,
$this->showRedisRestoreModal flag and $server->database_type to locate and
update the code.
🪄 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: CHILL

Plan: Pro

Run ID: bc296389-5792-4281-a44a-5bb4ec5861d5

📥 Commits

Reviewing files that changed from the base of the PR and between 2fdbf85 and ed1f060.

📒 Files selected for processing (3)
  • app/Livewire/DatabaseServer/Index.php
  • app/Policies/DatabaseServerPolicy.php
  • resources/views/livewire/database-server/index.blade.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Policies/DatabaseServerPolicy.php
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.php: In PHP files, always use curly braces for control structures, even for single-line bodies.
Use PHP 8 constructor property promotion: public function __construct(public GitHub $github) { }. Do not leave empty zero-parameter __construct() methods unless the constructor is private.
Use explicit return type declarations and type hints for all method parameters in PHP: function isAccessible(User $user, ?string $path = null): bool
Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
Use array shape type definitions in PHPDoc blocks for PHP.

Files:

  • resources/views/livewire/database-server/index.blade.php
  • app/Livewire/DatabaseServer/Index.php
resources/views/livewire/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

All Livewire component blade files should contain only view markup; all PHP logic must be in the component class.

Files:

  • resources/views/livewire/database-server/index.blade.php
resources/views/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

resources/views/**/*.blade.php: All UI components in Mary UI should be prefixed with x- (e.g., <x-button>, <x-input>, <x-card>) and use Heroicons for icons (e.g., icon="o-user" for outline, icon="s-user" for solid).
In Mary UI select components, use the :options prop with array format [['id' => 'value', 'name' => 'Label']].
In Mary UI alerts, use class="alert-success", class="alert-error", etc. (not variant prop).
Use Alpine.js for client-side interactions in Livewire instead of JavaScript frameworks.
In Blade templates, use :attr binding (dynamic syntax) instead of {{ }} interpolation when passing translated strings to component attributes to avoid double-encoding special characters.

Files:

  • resources/views/livewire/database-server/index.blade.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{php,blade.php}: Always use named routes and the route() function when generating links to other pages.
Translations should use __('...') helper function and be stored in JSON translation files in lang/{locale}.json. Keep technical terms like 'Backup', 'Restore', 'Snapshot(s)' in English across all locales.

Files:

  • resources/views/livewire/database-server/index.blade.php
  • app/Livewire/DatabaseServer/Index.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/Livewire/**/*.php: In Livewire components, use #[Validate] attributes or Form objects for validation. Call $this->validate() before processing data.
In Livewire components, use Session::flash() for one-time messages and show them via @if (session('success')) in Blade templates.
In Livewire components, return $this->redirect() with navigate: true for SPA-like navigation.
In Mary UI modals, add a boolean property to the component class and use wire:model in the Blade template.
Keep state server-side in Livewire components so the UI reflects it. Validate and authorize in actions as you would in HTTP requests.

Files:

  • app/Livewire/DatabaseServer/Index.php
🧠 Learnings (4)
📚 Learning: 2026-01-30T22:27:46.107Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 61
File: resources/views/livewire/volume/connectors/s3-config.blade.php:1-13
Timestamp: 2026-01-30T22:27:46.107Z
Learning: In Blade template files (any .blade.php) within the databasement project, allow using alert-info for informational content inside <x-alert> components. The guideline that permits alert-success and alert-error does not exclude using alert-info for informational purposes. Apply this consistently to all Blade components that render alerts; ensure semantic usage and accessibility.

Applied to files:

  • resources/views/livewire/database-server/index.blade.php
📚 Learning: 2026-02-06T10:34:43.585Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 75
File: resources/views/livewire/backup-job/_filters.blade.php:36-40
Timestamp: 2026-02-06T10:34:43.585Z
Learning: In Blade template files, when creating compact inline filter controls, prefer using native <input type="checkbox"> elements with daisyUI classes (e.g., checkbox checkbox-warning checkbox-xs) over the Mary UI <x-checkbox> component. The <x-checkbox> component adds wrapper markup (e.g., <div><fieldset><label> with gap-3) that can break tight inline flex layouts. Use the native input approach for compact inline controls, but reserve <x-checkbox> for form fields that require labels, hints, and errors.

Applied to files:

  • resources/views/livewire/database-server/index.blade.php
📚 Learning: 2026-02-25T10:48:17.811Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 132
File: app/Console/Commands/RecoverAgentLeasesCommand.php:44-48
Timestamp: 2026-02-25T10:48:17.811Z
Learning: When reviewing PHP code, especially with foreign keys that use cascadeOnDelete and are non-nullable, assume child relations exist at runtime (the database will delete children when the parent is deleted). Do not rely on null-safe operators for these relations, as PHPStan already models them as non-null. This guideline applies broadly to PHP files that define models with foreign keys using cascade delete; verify there are no unnecessary null checks or optional chaining on such relations.

Applied to files:

  • resources/views/livewire/database-server/index.blade.php
  • app/Livewire/DatabaseServer/Index.php
📚 Learning: 2026-02-18T09:45:52.485Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 116
File: app/Livewire/DatabaseServer/ConnectionStatus.php:18-18
Timestamp: 2026-02-18T09:45:52.485Z
Learning: In Livewire components, Eloquent model properties (e.g., public DatabaseServer $server) are automatically locked by the framework to prevent client-side ID tampering. The #[Locked] attribute is only needed for scalar properties (int, string, bool, etc.) that require protection from client-side mutation. Apply this guidance to all Livewire PHP components; use #[Locked] only on primitive properties that you want to shield from client manipulation, and rely on automatic locking for Eloquent model properties.

Applied to files:

  • app/Livewire/DatabaseServer/Index.php
🔇 Additional comments (1)
app/Livewire/DatabaseServer/Index.php (1)

203-210: Nice centralization of scoped listing logic.

Passing the scoped user into DatabaseServerQuery::buildFromParams() keeps the visibility rules in one place instead of reimplementing them in the component.

Comment thread app/Livewire/DatabaseServer/Index.php Outdated
…estore

Add authorize('view', \$server) before the Redis early-return so any
logged-in user cannot invoke confirmRestore with an arbitrary Redis server
ID without passing an authorization check. Move \$this->restoreId = \$id
to the same position so the modal's backup-jobs link always reflects the
correct server, even when the Redis instructions path is taken.
Copy link
Copy Markdown

@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)
app/Livewire/DatabaseServer/Index.php (1)

115-125: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Align the Redis branch with the restore policy.

Line 121 returns after only a view check, so any user who can see a Redis server can still open this restore-related flow even though DatabaseServerPolicy::restore() explicitly denies Redis servers. That leaves the Livewire action enforcing a weaker permission than the policy. Either gate this branch with the same restore ability or introduce a dedicated ability for viewing manual restore instructions and use that consistently in both the component and UI.

As per coding guidelines, "Keep state server-side in Livewire components so the UI reflects it. Validate and authorize in actions as you would in HTTP requests."

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

In `@app/Livewire/DatabaseServer/Index.php` around lines 115 - 125, The Redis
branch currently returns after only authorizing 'view', so allow-listing differs
from the policy; call $this->authorize('restore', $server) (the same ability
used later) before setting $this->restoreId and $this->showRedisRestoreModal
when $server->database_type === DatabaseType::REDIS, or alternatively introduce
and use a new ability name (e.g. 'viewManualRestore') consistently in the
Livewire component and UI; ensure you reference the DatabaseType::REDIS check,
the $this->restoreId and $this->showRedisRestoreModal assignments, and the
existing $this->authorize('restore', $server) usage so authorization is enforced
server-side before mutating component state.
🧹 Nitpick comments (1)
app/Livewire/DatabaseServer/Index.php (1)

205-209: ⚡ Quick win

Avoid calling isScopedUser() twice per render.

render() now checks isScopedUser() before calling DatabaseServerQuery::buildFromParams(), but that query builder already repeats the same check internally. Passing $user through and letting the query layer decide avoids an extra grant lookup for scoped users on every Livewire refresh.

♻️ Proposed simplification
         $servers = DatabaseServerQuery::buildFromParams(
             search: $this->search,
             sortColumn: $this->sortBy['column'],
             sortDirection: $this->sortBy['direction'],
-            scopedUser: $user->isScopedUser() ? $user : null,
+            scopedUser: $user,
         )->paginate(10);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Livewire/DatabaseServer/Index.php` around lines 205 - 209, Avoid calling
isScopedUser() here; pass the $user object to
DatabaseServerQuery::buildFromParams() and let the query layer decide scope.
Replace scopedUser: $user->isScopedUser() ? $user : null with scopedUser: $user
(keeping the existing earlier render() check intact), so the repeated grant
lookup in buildFromParams()/isScopedUser() is eliminated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/Livewire/DatabaseServer/Index.php`:
- Around line 115-125: The Redis branch currently returns after only authorizing
'view', so allow-listing differs from the policy; call
$this->authorize('restore', $server) (the same ability used later) before
setting $this->restoreId and $this->showRedisRestoreModal when
$server->database_type === DatabaseType::REDIS, or alternatively introduce and
use a new ability name (e.g. 'viewManualRestore') consistently in the Livewire
component and UI; ensure you reference the DatabaseType::REDIS check, the
$this->restoreId and $this->showRedisRestoreModal assignments, and the existing
$this->authorize('restore', $server) usage so authorization is enforced
server-side before mutating component state.

---

Nitpick comments:
In `@app/Livewire/DatabaseServer/Index.php`:
- Around line 205-209: Avoid calling isScopedUser() here; pass the $user object
to DatabaseServerQuery::buildFromParams() and let the query layer decide scope.
Replace scopedUser: $user->isScopedUser() ? $user : null with scopedUser: $user
(keeping the existing earlier render() check intact), so the repeated grant
lookup in buildFromParams()/isScopedUser() is eliminated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: af19d91c-52fe-499c-9472-e3ad727889ef

📥 Commits

Reviewing files that changed from the base of the PR and between ed1f060 and 8876b9b.

📒 Files selected for processing (1)
  • app/Livewire/DatabaseServer/Index.php
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.php: In PHP files, always use curly braces for control structures, even for single-line bodies.
Use PHP 8 constructor property promotion: public function __construct(public GitHub $github) { }. Do not leave empty zero-parameter __construct() methods unless the constructor is private.
Use explicit return type declarations and type hints for all method parameters in PHP: function isAccessible(User $user, ?string $path = null): bool
Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
Use array shape type definitions in PHPDoc blocks for PHP.

Files:

  • app/Livewire/DatabaseServer/Index.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/Livewire/**/*.php: In Livewire components, use #[Validate] attributes or Form objects for validation. Call $this->validate() before processing data.
In Livewire components, use Session::flash() for one-time messages and show them via @if (session('success')) in Blade templates.
In Livewire components, return $this->redirect() with navigate: true for SPA-like navigation.
In Mary UI modals, add a boolean property to the component class and use wire:model in the Blade template.
Keep state server-side in Livewire components so the UI reflects it. Validate and authorize in actions as you would in HTTP requests.

Files:

  • app/Livewire/DatabaseServer/Index.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{php,blade.php}: Always use named routes and the route() function when generating links to other pages.
Translations should use __('...') helper function and be stored in JSON translation files in lang/{locale}.json. Keep technical terms like 'Backup', 'Restore', 'Snapshot(s)' in English across all locales.

Files:

  • app/Livewire/DatabaseServer/Index.php
🧠 Learnings (2)
📚 Learning: 2026-02-18T09:45:52.485Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 116
File: app/Livewire/DatabaseServer/ConnectionStatus.php:18-18
Timestamp: 2026-02-18T09:45:52.485Z
Learning: In Livewire components, Eloquent model properties (e.g., public DatabaseServer $server) are automatically locked by the framework to prevent client-side ID tampering. The #[Locked] attribute is only needed for scalar properties (int, string, bool, etc.) that require protection from client-side mutation. Apply this guidance to all Livewire PHP components; use #[Locked] only on primitive properties that you want to shield from client manipulation, and rely on automatic locking for Eloquent model properties.

Applied to files:

  • app/Livewire/DatabaseServer/Index.php
📚 Learning: 2026-02-25T10:48:17.811Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 132
File: app/Console/Commands/RecoverAgentLeasesCommand.php:44-48
Timestamp: 2026-02-25T10:48:17.811Z
Learning: When reviewing PHP code, especially with foreign keys that use cascadeOnDelete and are non-nullable, assume child relations exist at runtime (the database will delete children when the parent is deleted). Do not rely on null-safe operators for these relations, as PHPStan already models them as non-null. This guideline applies broadly to PHP files that define models with foreign keys using cascade delete; verify there are no unnecessary null checks or optional chaining on such relations.

Applied to files:

  • app/Livewire/DatabaseServer/Index.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.

2 participants