Skip to content

fix: filter server groups by ownership and sharing status#10001

Open
lkmatsumura wants to merge 9 commits into
pgadmin-org:masterfrom
lkmatsumura:fix-get_all_server_groups
Open

fix: filter server groups by ownership and sharing status#10001
lkmatsumura wants to merge 9 commits into
pgadmin-org:masterfrom
lkmatsumura:fix-get_all_server_groups

Conversation

@lkmatsumura
Copy link
Copy Markdown

@lkmatsumura lkmatsumura commented Jun 2, 2026

Summary

Adds a new ADMIN_CAN_SEE_ALL_SERVERS configuration option for server mode and fixes server-group retrieval so admins only see allowed server groups and shared servers according to current settings.

Background

In server mode, administrator users were incorrectly seeing all servers and server groups. This branch restores the expected behavior by:

  • making admin-wide server visibility configurable,
  • preserving the previous behavior where admins see only owned groups plus groups containing shared servers,
  • respecting the Hide shared server preference when listing groups.

What changed

  • Added ADMIN_CAN_SEE_ALL_SERVERS = False in web/config.py
  • Updated web/pgadmin/model/__init__.py:
    • added the servers relationship on ServerGroup
    • corrected server/servergroup model relationships
  • Reworked access control in web/pgadmin/utils/server_access.py:
    • get_server() now honors ADMIN_CAN_SEE_ALL_SERVERS
    • get_server_groups_for_user() now returns owned groups plus shared-server groups only when allowed
    • added consistent admin and shared-server handling across server/group queries
  • Updated web/pgadmin/browser/server_groups/__init__.py:
    • ServerGroupView.get_all_server_groups() now uses the new access helper
    • group listing now respects hide_shared_server
    • shared-server detection logic was corrected

Verification / Test steps

  1. Run pgAdmin in server mode.
  2. Toggle ADMIN_CAN_SEE_ALL_SERVERS in web/config.py:
    • False: admin should see only owned groups and groups with shared servers.
    • True: admin sees all servers/groups.
  3. Ensure the browser preference Hide shared server hides shared-server groups as expected.
  4. Validate user behavior for both admin and normal users in server mode.
  5. Run related regression tests for server-group and server access behavior.

Docs

Recommend updating admin/server-mode configuration documentation with:

  • ADMIN_CAN_SEE_ALL_SERVERS
  • its default value (False)
  • how it affects admin visibility and server-group filtering

Risk

Low. The fix is configuration-driven and restores previous expected behavior when ADMIN_CAN_SEE_ALL_SERVERS remains disabled.

Summary by CodeRabbit

  • New Features

    • Admin option to control whether administrators can view all servers (defaults off).
  • Improvements

    • Listings consistently respect the "hide shared servers" preference.
    • Server/group listing logic optimized to determine shared groups in bulk, improving performance and consistency.
    • Administrator visibility now follows the new admin option.
  • Bug Fixes

    • Servers now appear under the correct server-group in tool responses.

…ver mode

- rewriting to return only admin user groups and groups with shared servers like the previous behaviour
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 64069b35-48a9-4113-ab79-4d92a745c484

📥 Commits

Reviewing files that changed from the base of the PR and between e226406 and d00a8ce.

📒 Files selected for processing (2)
  • web/pgadmin/browser/server_groups/__init__.py
  • web/pgadmin/utils/server_access.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/pgadmin/utils/server_access.py

Walkthrough

Adds ADMIN_CAN_SEE_ALL_SERVERS config, an explicit ServerGroup↔Server ORM relationship, refactors server/group access helpers to support only_owned and to respect the admin flag, and updates browser server-group code to bulk-prefetch shared-group IDs and use those for icon/visibility decisions.

Changes

Server & ServerGroup Visibility

Layer / File(s) Summary
Admin config
web/config.py
Adds ADMIN_CAN_SEE_ALL_SERVERS = False to gate admin visibility.
ServerGroup ORM relation
web/pgadmin/model/__init__.py
Adds ServerGroup.servers relationship and Server.servergroup inverse with back_populates and updated loading/cascade behavior.
Access helpers: server & group visibility
web/pgadmin/utils/server_access.py
get_server now requires ADMIN_CAN_SEE_ALL_SERVERS for admin-wide access; adds get_servers_from_group; get_server_group(gid, only_owned=False) and get_server_groups_for_user(only_owned=False) return queries and support owned + groups-with-shared union; get_user_server_query aligned with config flag.
Browser integration: predicates & listing
web/pgadmin/browser/server_groups/__init__.py
get_icon_css_class accepts shared_group_ids; has_shared_server/has_not_shared_server iterate get_servers_from_group; ServerGroupModule.get_nodes() bulk-fetches shared group IDs via ServerGroupView.get_shared_server_group_ids() and uses them during node build.
Tooling: grouping keys
web/pgadmin/tools/schema_diff/__init__.py, web/pgadmin/tools/sqleditor/__init__.py
Group servers by server.servergroup.name when assembling server lists/connection data.
sequenceDiagram
  participant Browser as ServerGroupView
  participant Access as server_access.get_server_groups_for_user
  participant DB as Database
  Browser->>Access: request visible groups (only_owned/hide_shared)
  Access->>DB: build/union queries for owned groups and groups with shared servers
  DB-->>Access: return query/results
  Access-->>Browser: return groups (possibly filtered) and shared_group_ids
Loading

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers:

  • adityatoshniwal
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objective of the PR: filtering server groups by ownership and sharing status according to the ADMIN_CAN_SEE_ALL_SERVERS configuration and user preferences.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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

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

@lkmatsumura lkmatsumura changed the title fix: get_all_server_groups function fix: filter server groups by ownership and sharing status Jun 2, 2026
- If hide_shared_server is true then show only user owned groups
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.

Caution

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

⚠️ Outside diff range comments (1)
web/pgadmin/browser/server_groups/__init__.py (1)

392-407: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

N+1 query problem when filtering server groups.

The list comprehension at lines 404-407 calls has_shared(group) for each non-owned group in server_groups. Each call to has_shared invokes ServerGroupModule.has_shared_server(group.id) (line 396), which executes a separate database query (lines 76-77). If there are N server groups, this can result in up to N additional database queries.

In environments with many users and server groups, this could significantly impact performance when administrators load the object explorer.

Consider optimizing by fetching all server group IDs that contain shared servers in a single query, then using set membership for the filter predicate.

⚡ Proposed optimization to eliminate N+1 queries
 `@staticmethod`
 def get_all_server_groups():
     """
     Returns the list of server groups to show in server mode.
     Includes groups owned by the user and groups containing
     shared servers accessible to this user.
     :return: server groups
     """
 
     # Don't display shared server if user has
     # selected 'Hide shared server'
     pref = Preferences.module('browser')
     hide_shared_server = pref.preference('hide_shared_server').get()
 
     server_groups = get_server_groups_for_user()
 
     def is_owner(group):
         return group.user_id == current_user.id
 
-    def has_shared(group):
-        return ServerGroupModule.has_shared_server(group.id)
+    # Fetch all group IDs containing shared servers in one query
+    shared_group_ids = {
+        s.servergroup_id for s in 
+        db.session.query(Server.servergroup_id).filter(
+            Server.shared == True
+        ).distinct()
+    }
+    
+    def has_shared(group):
+        return group.id in shared_group_ids
 
     if hide_shared_server:
         return [
             group for group in server_groups
             if is_owner(group)
         ]
 
     return [
         group for group in server_groups
         if is_owner(group) or has_shared(group)
     ]
🤖 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 `@web/pgadmin/browser/server_groups/__init__.py` around lines 392 - 407, The
current filter uses has_shared(group) which calls
ServerGroupModule.has_shared_server(group.id) per group causing an N+1 DB query;
instead, before filtering (when hide_shared_server is False) fetch all
server-group IDs that contain shared servers in one query (e.g., add or call a
single-method like ServerGroupModule.get_shared_server_group_ids() or a
classmethod that returns a set/list of ids), store that result in a set
(shared_group_ids), and then change the predicate to check membership (group.id
in shared_group_ids) together with is_owner(group) when constructing the final
server_groups list; keep is_owner as-is and only replace repeated has_shared
calls with set membership to eliminate the per-group DB queries.
🤖 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.

Outside diff comments:
In `@web/pgadmin/browser/server_groups/__init__.py`:
- Around line 392-407: The current filter uses has_shared(group) which calls
ServerGroupModule.has_shared_server(group.id) per group causing an N+1 DB query;
instead, before filtering (when hide_shared_server is False) fetch all
server-group IDs that contain shared servers in one query (e.g., add or call a
single-method like ServerGroupModule.get_shared_server_group_ids() or a
classmethod that returns a set/list of ids), store that result in a set
(shared_group_ids), and then change the predicate to check membership (group.id
in shared_group_ids) together with is_owner(group) when constructing the final
server_groups list; keep is_owner as-is and only replace repeated has_shared
calls with set membership to eliminate the per-group DB queries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 30238474-adba-4a60-9852-77f918344829

📥 Commits

Reviewing files that changed from the base of the PR and between 8108ed6 and cdb686b.

📒 Files selected for processing (1)
  • web/pgadmin/browser/server_groups/__init__.py

- Create ADMIN_CAN_SEE_ALL_SERVERS config
- If false admin see only owned and shared server from other administrators
  same as a common user
- Change in ServerGroup model to allow servers relationship
- Change in get_server_groups_for_user of server_access.py
  to facilitate to meet the criterias
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: 3

🤖 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 `@web/pgadmin/browser/server_groups/__init__.py`:
- Around line 412-414: The returned server_groups list from
get_server_groups_for_user should preserve the current user's default group at
index 0 so ServerGroupModule.get_nodes() (which checks idx == 0 to prevent
delete) continues to work; after materializing server_groups =
get_server_groups_for_user(...).all(), find the current user's default group id
(or object) and reorder the list so that default group is first (move it to
index 0) before returning; update references to server_groups,
get_server_groups_for_user, and ensure compatibility with
ServerGroupModule.get_nodes() which relies on position rather than changing that
caller.

In `@web/pgadmin/model/__init__.py`:
- Line 255: The ServerGroup.servers and Server.servers relationships are defined
over the same FK (server.servergroup_id → servergroup.id) causing overlapping
SQLAlchemy relationships; update the pair to be a single coherent relationship
by replacing the implicit backrefs with an explicit paired relationship using
back_populates on both sides (e.g., in ServerGroup define servers =
relationship('Server', back_populates='servergroup', ...) and in Server define
servergroup = relationship('ServerGroup', back_populates='servers', ...) ), or
if you intentionally need two relationships over the same join, mark the
intentional overlap with overlaps='servers,servergroup' on the relationship
definitions to suppress mapper warnings and keep in-memory state consistent.

In `@web/pgadmin/utils/server_access.py`:
- Around line 74-86: get_servers_from_group currently returns all Server rows
for a group when only_owned is False, exposing other users' non-shared servers
to callers like ServerGroupModule.has_shared_server/has_not_shared_server;
update get_servers_from_group to apply the same access predicate used elsewhere
by calling or composing get_user_server_query(servergroup_id=gid) (or reapply
the "owned or shared" filter that get_user_server_query enforces) so that the
returned query only includes servers visible to the current user while
preserving the only_owned branch behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e0754ca-3b4f-4fe7-8d6d-3ebce6ebef67

📥 Commits

Reviewing files that changed from the base of the PR and between cdb686b and 634c7d8.

📒 Files selected for processing (4)
  • web/config.py
  • web/pgadmin/browser/server_groups/__init__.py
  • web/pgadmin/model/__init__.py
  • web/pgadmin/utils/server_access.py

Comment on lines 412 to 414
server_groups = get_server_groups_for_user( only_owned=hide_shared_server).all()

return server_groups
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the current user's default group first before returning this list.

ServerGroupModule.get_nodes() still uses list position at Line 122 (idx == 0) to decide which group cannot be deleted. After this refactor, .all() materializes an unordered result set that can include other users' shared groups, so the wrong node can lose its delete action while the actual default group appears deletable until the delete endpoint rejects it.

One safe way to preserve the existing UI contract
-        server_groups = get_server_groups_for_user( only_owned=hide_shared_server).all()
-
-        return server_groups
+        server_groups = get_server_groups_for_user(
+            only_owned=hide_shared_server
+        ).all()
+        server_groups.sort(key=lambda g: (g.user_id != current_user.id, g.id))
+        return server_groups
🤖 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 `@web/pgadmin/browser/server_groups/__init__.py` around lines 412 - 414, The
returned server_groups list from get_server_groups_for_user should preserve the
current user's default group at index 0 so ServerGroupModule.get_nodes() (which
checks idx == 0 to prevent delete) continues to work; after materializing
server_groups = get_server_groups_for_user(...).all(), find the current user's
default group id (or object) and reorder the list so that default group is first
(move it to index 0) before returning; update references to server_groups,
get_server_groups_for_user, and ensure compatibility with
ServerGroupModule.get_nodes() which relies on position rather than changing that
caller.

Comment thread web/pgadmin/model/__init__.py Outdated
Comment thread web/pgadmin/utils/server_access.py Outdated
Luiz Matsumura added 2 commits June 3, 2026 11:47
do not need to check if is server mode anymore or note in get_nodes
- Refactoring as suggested due potential major issue to avoid
  expose other users non shared servers
@lkmatsumura lkmatsumura marked this pull request as draft June 3, 2026 15:17
- Adjusting relationship
  - Server has one ServerGroup not servers
  - Using back_populates instead of backref for better control

- Adjusted references to ServerGroup in server:
  server.servergroup.name instead server.servers.name
@lkmatsumura lkmatsumura marked this pull request as ready for review June 3, 2026 15:39
@lkmatsumura
Copy link
Copy Markdown
Author

May fix #9933

Copy link
Copy Markdown
Author

@lkmatsumura lkmatsumura left a comment

Choose a reason for hiding this comment

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

Hi, i finished the changes coderrabit demanded.
I think it is ready for maintaners review.
Hope to this changes to be useful, thanks for this great tool.

- Added ServerGroupView.get_shared_server_group_ids() to fetch all shared-server group IDs in one query
- Updated ServerGroupModule.get_nodes() to use pre-fetched
- Changed get_icon_css_class() to use membership check instead of repeated has_shared_server() callsID set
@lkmatsumura
Copy link
Copy Markdown
Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

✅ Action performed

Reviews resumed.

@lkmatsumura
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

✅ Action performed

Full review finished.

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

♻️ Duplicate comments (1)
web/pgadmin/browser/server_groups/__init__.py (1)

118-133: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't derive deleteability from the visible list index.

Once shared groups are mixed into this ordered result, idx == 0 can point at another user's shared group with a lower global id. The real default owned group then renders as deletable until the delete endpoint rejects it. Compare against the first owned group's id explicitly instead of relying on list position.

🤖 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 `@web/pgadmin/browser/server_groups/__init__.py` around lines 118 - 133, The
code currently uses the visible list index to decide deletability, which is
wrong when shared groups are mixed in; instead, compute the first owned group's
id up-front by scanning ServerGroupView.get_all_server_groups() for the first
group whose user_id matches the current user's id (or otherwise identifies
ownership), store it (e.g. first_owned_group_id), and then when yielding nodes
in the loop use can_delete = True if group.id != first_owned_group_id (and still
True only for owned groups), updating the logic around get_icon_css_class and
generate_browser_node accordingly so deletability is based on group.id compared
to first_owned_group_id rather than enumerate index.
🤖 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 `@web/pgadmin/browser/server_groups/__init__.py`:
- Around line 436-438: The current filter uses Server.shared directly on a
ServerGroup query (in the block using get_server_groups_for_user and producing
group_ids), which doesn't join servers and thus treats the predicate
incorrectly; update the filtering to correlate Server to ServerGroup (e.g. join
Server or use an EXISTS subquery referencing Server.servergroup_id ==
ServerGroup.id and Server.shared) so the filter only selects groups that
actually have shared servers before building group_ids from the
get_server_groups_for_user(...) query.

In `@web/pgadmin/utils/server_access.py`:
- Around line 103-104: The admin fast-paths in get_server_group and
get_server_groups_for_user currently ignore the only_owned flag when
config.ADMIN_CAN_SEE_ALL_SERVERS is true; change those branches to honor
only_owned by returning a filtered query when only_owned is True (e.g. add an
ownership filter using the ServerGroup ownership field or relation and the
current user id) and only bypass ownership checks when only_owned is False.
Specifically update the logic around _is_admin() &&
config.ADMIN_CAN_SEE_ALL_SERVERS to conditionally apply an ownership filter
(using ServerGroup.query.filter_by(..., owned_by=current_user.id) or equivalent)
so both get_server_group(...) and get_server_groups_for_user(...) respect
only_owned.

---

Duplicate comments:
In `@web/pgadmin/browser/server_groups/__init__.py`:
- Around line 118-133: The code currently uses the visible list index to decide
deletability, which is wrong when shared groups are mixed in; instead, compute
the first owned group's id up-front by scanning
ServerGroupView.get_all_server_groups() for the first group whose user_id
matches the current user's id (or otherwise identifies ownership), store it
(e.g. first_owned_group_id), and then when yielding nodes in the loop use
can_delete = True if group.id != first_owned_group_id (and still True only for
owned groups), updating the logic around get_icon_css_class and
generate_browser_node accordingly so deletability is based on group.id compared
to first_owned_group_id rather than enumerate index.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3c949fc0-4aa9-43a9-a360-cc4217095747

📥 Commits

Reviewing files that changed from the base of the PR and between 0d11dbc and e226406.

📒 Files selected for processing (6)
  • web/config.py
  • web/pgadmin/browser/server_groups/__init__.py
  • web/pgadmin/model/__init__.py
  • web/pgadmin/tools/schema_diff/__init__.py
  • web/pgadmin/tools/sqleditor/__init__.py
  • web/pgadmin/utils/server_access.py

Comment thread web/pgadmin/browser/server_groups/__init__.py Outdated
Comment thread web/pgadmin/utils/server_access.py
Luiz Matsumura added 2 commits June 3, 2026 16:15
…roup

- Enforce that the id of first owned group id is not a shared group
- even if the server or group is not owned by the admin, if
  config.ADMIN_CAN_SEE_ALL_SERVERS this is de desired behaviour
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