Skip to content

refactor(Servergroup): Refactor server group access and shared-group handling#10077

Open
lkmatsumura wants to merge 12 commits into
pgadmin-org:masterfrom
lkmatsumura:servergroup_refactor
Open

refactor(Servergroup): Refactor server group access and shared-group handling#10077
lkmatsumura wants to merge 12 commits into
pgadmin-org:masterfrom
lkmatsumura:servergroup_refactor

Conversation

@lkmatsumura

@lkmatsumura lkmatsumura commented Jun 10, 2026

Copy link
Copy Markdown

Summary

Refactor server group access and shared-group handling in pgAdmin4 to centralize visibility logic and simplify browser rendering.

What changed

  • Updated web/pgadmin/utils/server_access.py

    • Added get_server_groups_for_user() and get_server_groups_for_user_query()
    • Simplified get_server_group() to reuse shared access logic
    • Added is_shared_group metadata on returned groups
    • Consolidated shared-group filtering, ownership checks, and ordering
  • Updated web/pgadmin/browser/server_groups/__init__.py

    • Replaced ad hoc shared-server checks with centralized access helpers
    • Removed redundant ServerGroupModule.has_shared_server() logic
    • Ensured shared server groups are rendered with the correct icon
    • Prevented deletion of shared server groups
    • Improved deletion error messages for shared groups and first group restrictions

Notes

  • This PR focuses on server group refactor work.

Summary by CodeRabbit

  • Bug Fixes

    • Improved deletion error handling for server groups that contain shared servers and for deletion attempts affecting the first server group.
  • Improvements

    • Refined server-group visibility, shared-state handling, and icon selection across listings and tree nodes.
    • Updated server-group lookup and targeted retrieval to better respect shared/hide behavior.
    • Improved UI permissions by deriving edit/drop actions from the server-group’s provided flags.

Luiz K Matsumura added 3 commits June 10, 2026 12:34
…s a shared group.

- Added is_shared_group property to the result to simplify
  the identificantion of shared group- Added is_shared_group property
  to the result to simplify the identification of shared group

- rules centralized in get_server_groups_for_user_query() function
…and shared group logic

- change to call get_server_groups_for_user() to retrive the servergroups
  when possible
- same for get_server_group()
- Simplifying the method of detecting whether the group is shared or not
  using the information now returned by get_server_groups_for_user() and get_server_group().
- A shared group is not allowed to be deleted.
- Only the owner can do it.
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

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

Refactored server-group access and icon selection by introducing new query helpers to compute is_shared_group visibility, updating icon selection to accept shared status directly, and revising view operations (delete, update, create, nodes) to use the new access layer with more specific error messages. Moved server-group permission logic from client-side ownership checks to data-driven flags in node responses.

Changes

Server Group Access and Icon Refactoring

Layer / File(s) Summary
Data access helpers and query construction
web/pgadmin/utils/server_access.py
Updated SQLAlchemy imports to include case and exists; added get_server_group(gid, hide_shared=False) that delegates to visibility helpers; refactored get_server_groups_for_user() to accept hide_shared and servergroup_id filters and materialize is_shared_group and is_first_user_group annotations; introduced get_server_groups_for_user_query() to build visibility queries with desktop/server branching, shared-server existence checks, and ownership-based ordering.
Icon selection refactor
web/pgadmin/browser/server_groups/__init__.py
Updated get_icon_css_class(is_shared_group, default_val) to accept shared-group status directly instead of group id/user id pair.
Server group node generation
web/pgadmin/browser/server_groups/__init__.py
Refactored ServerGroupModule.get_nodes() to derive shared state from group.is_shared_group; updated ServerGroupView.nodes() endpoints to compute icon and can_delete from annotation; updated listing behavior to use get_server_groups_for_user(hide_shared=True) in non-server mode when gid is unset.
Server group view operations (CRUD)
web/pgadmin/browser/server_groups/__init__.py
Updated ServerGroupView.delete with distinct error messages for shared-server and first-group cases; refactored ServerGroupView.update to fetch via get_server_group(gid, hide_shared=True) and use group.is_shared_group for response computation; updated ServerGroupView.create to refresh created group via get_server_group(sg.id) before response generation; preserved exception handling with rollback semantics.
Client-side permission controls
web/pgadmin/browser/server_groups/static/js/server_group.js
Updated AMD module to remove lodash and current_user dependencies; replaced ownership-based permission logic in canEdit and canDrop with simple flags from server-provided itemData; removed client-side canDelete constraint that prevented deletion of the last server group, now enforced server-side.

Sequence Diagram

sequenceDiagram
  participant ServerGroupView
  participant get_server_group
  participant get_server_groups_for_user
  participant get_server_groups_for_user_query
  participant Database
  
  ServerGroupView->>get_server_group: get_server_group(gid, hide_shared=True)
  get_server_group->>get_server_groups_for_user: servergroup_id=gid, hide_shared
  get_server_groups_for_user->>get_server_groups_for_user_query: build visibility query
  get_server_groups_for_user_query->>Database: SELECT with is_shared_group case/exists logic
  Database-->>get_server_groups_for_user_query: groups with is_shared_group annotation
  get_server_groups_for_user-->>get_server_group: annotated group list
  get_server_group-->>ServerGroupView: single group or None
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • pgadmin-org/pgadmin4#10006: Overlapping refactoring of get_server_group() and get_server_groups_for_user*() helpers in web/pgadmin/utils/server_access.py to modify visibility logic and is_shared_group computation.
🚥 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 PR title 'refactor(Servergroup): Refactor server group access and shared-group handling' directly and accurately summarizes the main changes: refactoring server group access logic and handling of shared groups across multiple backend files.
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
web/pgadmin/utils/server_access.py (1)

133-133: ⚡ Quick win

Use Server.shared instead of Server.shared == True in SQLAlchemy filter.

In SQLAlchemy, boolean columns can be used directly in filter expressions. Using == True is redundant and flagged by linters.

♻️ Proposed fix
             .filter(
                 Server.servergroup_id == ServerGroup.id,
-                Server.shared == True
+                Server.shared
             )
🤖 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/utils/server_access.py` at line 133, The SQLAlchemy filter
currently uses the redundant comparison Server.shared == True; replace that
expression with the boolean column itself (Server.shared) wherever used in the
query/filter (e.g., in the function or method that constructs the query
referencing Server.shared) so the filter reads simply Server.shared to satisfy
linters and follow SQLAlchemy conventions.

Source: Linters/SAST tools

🤖 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.

Nitpick comments:
In `@web/pgadmin/utils/server_access.py`:
- Line 133: The SQLAlchemy filter currently uses the redundant comparison
Server.shared == True; replace that expression with the boolean column itself
(Server.shared) wherever used in the query/filter (e.g., in the function or
method that constructs the query referencing Server.shared) so the filter reads
simply Server.shared to satisfy linters and follow SQLAlchemy conventions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66b9468b-6998-4f8c-b411-8278e08db886

📥 Commits

Reviewing files that changed from the base of the PR and between 04fa05c and 6dd0f01.

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

@dpage dpage left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for taking this on — centralizing the visibility logic in server_access.py and replacing the per-server has_shared_server() loop with a single SQL EXISTS is a nice direction. A few things need addressing before this is mergeable:

1. Desktop mode is broken — (0).label('is_shared_group') raises AttributeError.
In get_server_groups_for_user_query(), the non-SERVER_MODE branch does:

ServerGroup.query.add_columns((0).label('is_shared_group'))

int has no .label() method, so this throws at query-build time (verified locally). Since get_nodes() was also changed to always route through get_all_server_groups(), the server-group tree fails to render in desktop mode — the default deployment. Use from sqlalchemy import literal and literal(0).label('is_shared_group').

2. Access regression in update().
update() previously fetched ServerGroup.query.filter_by(user_id=current_user.id, id=gid). It now uses get_server_group(gid), which in SERVER_MODE returns groups the user does not own as long as they contain a shared server. The method then does servergroup.name = data['name']; db.session.commit() with no ownership guard, so a user can rename another user's group. Deletion is correctly gated against shared groups (can_delete), but rename needs the same owner check.

3. Style checks will fail CI.

  • Server.shared == True → E712 (use Server.shared.is_(True) or just Server.shared).
  • Only one blank line between the two new top-level functions → E302.
  • A couple of lines exceed 79 chars (the .all() call and a docstring line) → E501.
  • for group, is_shared in sg: has a double space.

4. No tests, no changelog entry. A refactor touching access control should have a test for the owned / shared / desktop paths, and docs/en_US/release_notes_*.rst needs a line.

Since this isn't tied to a reported issue, it'd also help to note what user-facing problem it's solving — the diff reads as pure cleanup, so the bar is "demonstrably no behaviour change," and right now #1/#2 are behaviour changes.

Luiz K Matsumura added 4 commits June 15, 2026 15:39
- In Desktop Mode get_server_groups_for_user_query() raise the error
- User cannot update a shared Group, only owned Server Groups
- Class ServerGroupView of server_groups
  in desktop mode pref.preferences('hide_shared_server')
  returns none. Included a treatment to check if it is None
  then hide per default (anyway there are no shared servers
  in desktop mode)
- 79 char per line limit
- 2 blank line between top-level functions
- E712 in Server.shared == True
- double space in `for group, is_shared  in sg:`
@lkmatsumura

Copy link
Copy Markdown
Author

@dpage, Thank's by the review.
I don't know what and how to do the item 4. Hope the changes commited fixes the itens 1,2 and 3.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)

386-398: ⚠️ Potential issue | 🟡 Minor

Add can_delete parameter to match get_nodes() behavior.

In get_nodes() (line 79), can_delete is explicitly set based on idx > 0 and not group.is_shared_group. The nodes() method (lines 386–398) omits this parameter entirely. Both methods return similar node structures but with inconsistent delete permissions, creating a UX issue where the delete action may be displayed for the first group or shared groups via the nodes() endpoint while hidden via get_nodes().

Apply the same can_delete logic to the nodes() method:

can_delete=True if [group condition] and not group.is_shared_group else False

The server-side delete() method still enforces the restriction, so this is not a security issue—but it prevents confusing UX where the delete action is shown but fails.

🤖 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 386 - 398, The
nodes() method in the loop iterating through groups (lines 386-398) is missing
the can_delete parameter that exists in get_nodes() at line 79. To fix this,
modify the loop to use enumerate() to track the index, then add the can_delete
parameter to the generate_browser_node() call with the logic: can_delete should
be True only when the group index is greater than 0 AND the group is not a
shared group (not group.is_shared_group), matching the behavior of get_nodes().
🤖 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 386-398: The nodes() method in the loop iterating through groups
(lines 386-398) is missing the can_delete parameter that exists in get_nodes()
at line 79. To fix this, modify the loop to use enumerate() to track the index,
then add the can_delete parameter to the generate_browser_node() call with the
logic: can_delete should be True only when the group index is greater than 0 AND
the group is not a shared group (not group.is_shared_group), matching the
behavior of get_nodes().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 07569480-97dc-44fd-90f8-8e877f4272ff

📥 Commits

Reviewing files that changed from the base of the PR and between ceb3401 and c7ff6eb.

📒 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

Luiz K Matsumura added 2 commits June 16, 2026 11:11
- To facilitate and to have a more reliable way to determine if
 the servergroup is the first servergroup owned by the user
 changed the query to return this info
…rking properly

servergroups\__init__.py:
- nodes now return can_delete and cad_edit properties to the script handle it more easily and not expose the rules
- uniformize information generate  passed to self.blueprint.generate_browser_node() function

server_groups.js
- canDelete was not used so droped it
- Adjusted canDrop to use the can_delete information from itemData
- Added canEdit to control if the properties can be edited or not

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@web/pgadmin/browser/server_groups/static/js/server_group.js`:
- Around line 11-14: Remove the unused `pgadmin.user_management.current_user`
dependency from the dependency array in the define() call for
`pgadmin.node.server_group`. The dependency is listed in the array but is not
passed as a parameter to the factory function (it's not in the function
signature with gettext, url_for, and pgAdmin), indicating it is dead code that
should be removed from the array.
🪄 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: f61f60fd-f6db-426d-9be4-6e1ff99b808f

📥 Commits

Reviewing files that changed from the base of the PR and between c7ff6eb and c8a0c85.

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

Comment thread web/pgadmin/browser/server_groups/static/js/server_group.js
- removed unused dependency from the dependency array
@lkmatsumura lkmatsumura requested a review from dpage June 16, 2026 14:45
@dpage

dpage commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Thanks for the updates @lkmatsumura, this has come a long way. Items 1 and 2 from my earlier review are sorted: desktop mode no longer throws (the literal(0) fix), and update() is now owner-gated via get_server_group(gid, hide_shared=True) with a proper None guard, so a user can no longer rename another user's group. The unused current_user import and the missing can_delete on nodes() are also resolved. Two things still need attention before this is mergeable, and then there's item 4.

1. make check-pep8 is currently failing (CI red, 25 violations). Running pycodestyle --config=.pycodestyle over the two Python files, the ones to fix are:

  • utils/server_access.py: E201/E202 (stray spaces in ( ServerGroup... and hide_shared=hide_shared )), E126/E131 continuation-indent on the return ( ServerGroup.query.add_columns(...) blocks, and E303 (two blank lines inside the function, ~line 148).
  • browser/server_groups/__init__.py: E501 on the two new long error-message strings (lines 185 and 194) and line 391; plus W503/E128 on the can_delete=(not (... \n or ...)) and get_icon_css_class boolean continuations. Note our config ignores W504 but not W503, so a line break before and/or trips it: move the operator to the end of the previous line, and wrap the long error strings across two lines.

2. canEdit regression: can_edit is missing from three of the five node paths. The base Node.canEdit defaults to true, and this PR overrides it to read itemData.can_edit. But can_edit is only passed in get_nodes() and update(); it's omitted in create() and both branches of nodes(). Nodes from those paths therefore get can_edit === undefined, which disables Edit/Properties even for the user's own groups (e.g. a group can't be renamed right after it's created, until a tree refresh). can_delete is set in all five paths, so canDrop is fine; can_edit just needs the same treatment, can_edit=(not <group>.is_shared_group) in those three calls (always True for the freshly-created group in create()).

On item 4 (the part you were unsure about):

  • Changelog: you can skip it. We've recently moved to adding release-notes entries in a batch just before release rather than per-PR, so there's no need to touch release_notes_*.rst here.
  • Tests: extend the existing suite. There's already web/pgadmin/browser/server_groups/tests/, and test_sg_data_isolation.py and test_sg_get.py are good models. The behaviours worth covering are the ones this refactor reshapes: an owned group is visible/editable/deletable; the first owned group is not deletable; a group containing another user's shared server is visible but not editable or deletable (and reports is_shared_group=True with the shared icon); and the desktop path returns owned groups with is_shared_group=0. Following the shape of test_sg_data_isolation.py is the most direct route.

One minor aside, not blocking: in get_all_server_groups() the hide_shared_server fallback defaults to True (hide) when the preference object is absent, which is the opposite of the preference's registered default; defaulting to the registered value would be marginally tidier, though that path shouldn't normally occur.

Thanks again, the centralised approach reads well, it's mostly the lint and the can_edit consistency now.

Luiz K Matsumura added 2 commits June 19, 2026 11:40
  - fix of make check-pep8 style errors detected
  - Adding can_edit to create() and nodes()
  - hide_shared_server default to false if the pref  object is absent
- Added a flush after adding a new server group to retrieve its ID immediately.
- Updated the server group retrieval to include access metadata.
- Enhanced the query for server groups to filter by ID when provided, ensuring more precise results.
@lkmatsumura

lkmatsumura commented Jun 22, 2026

Copy link
Copy Markdown
Author

@dpage I think the style code is ok now, The tests since I don't know how to write it, is AI generated, but it's seens correct to me.

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