Skip to content

fix: SharedServer feature parity columns and write guards#9835

Open
asheshv wants to merge 1 commit intomasterfrom
audit/shared-server-issues
Open

fix: SharedServer feature parity columns and write guards#9835
asheshv wants to merge 1 commit intomasterfrom
audit/shared-server-issues

Conversation

@asheshv
Copy link
Copy Markdown
Contributor

@asheshv asheshv commented Apr 9, 2026

Summary

  • Adds 5 columns to SharedServer model (passexec_cmd, passexec_expiration, kerberos_conn, tags, post_connection_sql) for feature parity with Server, so non-owners get per-user values via overlay instead of inheriting/suppressing owner fields
  • Blocks non-owners from setting dangerous fields (passexec_cmd, passexec_expiration, post_connection_sql, db_res, db_res_type) via _owner_only_fields write guard in _set_valid_attr_value()
  • Retains driver-level suppression of passexec/post_connection_sql on ServerManager for non-owners as defense-in-depth, including re-suppression after manager.update() in the connect() endpoint
  • Adds null guard in get_shared_server_properties() for None sharedserver
  • Migration: orphan cleanup (deletes SharedServer rows referencing deleted Servers), column-exists guard, unique constraint idempotency guard

Builds on top of #9830.

Test plan

  • Verify migration runs cleanly on fresh DB (SQLite)
  • Verify migration runs cleanly on existing DB with orphaned SharedServer rows
  • Run cd web && python regression/runtests.py --pkg browser.server_groups.servers --modules test_shared_server_unit — all unit tests pass
  • In server mode: verify non-owner cannot set passexec_cmd or post_connection_sql via PUT to /browser/server/obj/
  • In server mode: verify non-owner connecting to shared server does NOT execute owner's passexec_cmd
  • In server mode: verify overlay correctly shows SharedServer's own kerberos_conn, tags, post_connection_sql values

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Shared servers now support additional configuration fields: command execution settings, expiration time, Kerberos connection support, custom tags, and post-connection SQL scripting.
  • Improvements

    • Enhanced shared server property handling and strengthened access controls to prevent non-owners from modifying sensitive configuration settings.

Add passexec_cmd, passexec_expiration, kerberos_conn, tags, and
post_connection_sql columns to SharedServer so non-owners can have
per-user customizations overlaid from their own record instead of
inheriting (or suppressing) the owner's values.

Security hardening:
- Non-owners blocked from setting passexec_cmd, passexec_expiration,
  post_connection_sql, db_res, db_res_type via _owner_only_fields guard
- Driver-level suppression retained for connection layer defense-in-depth
- Re-suppression after manager.update() in connect() endpoint
- SharedServer creation defaults dangerous fields to None/False

Migration improvements:
- Orphan cleanup: delete SharedServer rows referencing deleted Servers
- Column-exists guard prevents failures on migration re-runs
- Unique constraint idempotency guard via get_unique_constraints()

Other fixes:
- Null guard in get_shared_server_properties() for None sharedserver
- Unit tests updated for overlay semantics with full field coverage
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Walkthrough

Extended the SharedServer model with five new columns (passexec_cmd, passexec_expiration, kerberos_conn, tags, post_connection_sql), updated the database migration to handle schema evolution with orphaned row cleanup and conditional constraint creation, and refactored server property overlay logic to merge these fields from SharedServer records instead of suppressing them.

Changes

Cohort / File(s) Summary
Database schema & migration
web/migrations/versions/add_user_id_to_debugger_func_args_.py
Replaces unconditional unique constraint creation with orphaned row cleanup, duplicate removal (database-specific logic), and conditional column/constraint additions guarded for re-runs.
SharedServer model
web/pgadmin/model/__init__.py
Adds five new nullable/defaulted columns: passexec_cmd (Text), passexec_expiration (Integer), kerberos_conn (Boolean, default 0), tags (JSON), and post_connection_sql (String).
Server property merge & access control
web/pgadmin/browser/server_groups/servers/__init__.py
Changes shared-server property overlay from suppression to overlay pattern for new fields; tightens non-owner update rules via _set_valid_attr_value field allowlist; adds post-update resuppression in connect() for non-owners.
Tests & scaffolding
web/pgadmin/browser/server_groups/servers/tests/test_shared_server_unit.py
Updates merge assertions from suppression to overlay verification; adds kerberos_conn and tags coverage; introduces TestOwnerOnlyFieldsGuard to validate permission-based field access; extends test fixture defaults.
Driver documentation
web/pgadmin/utils/driver/psycopg3/__init__.py
Clarifies inline comments distinguishing connection-layer field suppression from UI-layer overlay handling; no functional changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pgadmin-org/pgadmin4#9830 — Modifies shared-server schema, migration structure, and constraint/column management in the same areas, with overlapping model and alembic changes.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding SharedServer feature parity columns and implementing write guards to prevent non-owners from modifying sensitive fields.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit/shared-server-issues

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

Inline comments:
In `@web/migrations/versions/add_user_id_to_debugger_func_args_.py`:
- Around line 161-164: The migration defines the 'kerberos_conn' column with
server_default='false'; change this to use SQLAlchemy's portable boolean literal
by setting server_default=sa.false() on the sa.Column(...) for 'kerberos_conn'
(and ensure sa.false is available from the imported sa namespace if not
already). This ensures the default renders as a proper SQL boolean literal
across DB backends.

In `@web/pgadmin/browser/server_groups/servers/__init__.py`:
- Around line 1182-1184: The current truthiness check for passexec_expiration
collapses an explicit 0 to None; change the conditional to test explicitly for
None (e.g. use "if server.passexec_expiration is not None else None") so that a
value of 0 is preserved when building the properties dict for
passexec_expiration and will round-trip correctly through the properties API.
- Around line 230-231: The update path currently computes tag deltas using
server.tags (the owner's tags) which causes non-owner SharedServer.tags to be
rebuilt from the owner; change the merge base so that when handling a non-owner
update (where SharedServer exists and you assign server.kerberos_conn =
sharedserver.kerberos_conn and server.tags = sharedserver.tags) you compute tag
diffs against sharedserver.tags (i.e. use SharedServer.tags as the base) instead
of server.tags inside update(), and apply only the delta to the non-owner
SharedServer record to avoid overwriting its existing tags.

In `@web/pgadmin/model/__init__.py`:
- Around line 563-569: The SCHEMA_VERSION constant was not incremented after
adding new SharedServer columns (passexec_cmd, passexec_expiration,
kerberos_conn, tags, post_connection_sql), so update the module's SCHEMA_VERSION
(e.g., from 50 to 51) where it is defined so upgrades detect and run the
migration for the SharedServer changes; ensure the new version is used by
migration/upgrade logic and any tests referencing SCHEMA_VERSION are updated
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 49a1e807-6d76-4299-b568-5be7ba3c9c6e

📥 Commits

Reviewing files that changed from the base of the PR and between 9a76ed8 and 0fd0ac3.

📒 Files selected for processing (5)
  • web/migrations/versions/add_user_id_to_debugger_func_args_.py
  • web/pgadmin/browser/server_groups/servers/__init__.py
  • web/pgadmin/browser/server_groups/servers/tests/test_shared_server_unit.py
  • web/pgadmin/model/__init__.py
  • web/pgadmin/utils/driver/psycopg3/__init__.py

Comment on lines +161 to +164
('kerberos_conn',
sa.Column('kerberos_conn', sa.Boolean(),
nullable=False,
server_default='false')),
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

🧩 Analysis chain

🌐 Web query:

In SQLAlchemy/Alembic, for a Boolean column, does server_default='false'render as a quoted string literal or a SQL boolean literal, and isserver_default=sa.text('false')orsa.false() the recommended portable form?

💡 Result:

  • server_default='false' is treated as a SQL expression string, not a Python boolean. In DDL it will typically render as DEFAULT false (unquoted), i.e. not DEFAULT 'false'—SQLAlchemy only applies string-literal quoting rules when the default string itself represents a quoted literal value. [1]
  • server_default=sa.text('false') likewise renders DEFAULT false; it’s just the explicit “this is SQL text” form (SQLAlchemy docs advise text() over raw strings for literal SQL defaults). [2]
  • For the most portable/cross-dialect boolean server default, prefer server_default=sa.false() (or sa.true()), since SQLAlchemy can adapt true/false constants on backends that don’t support native boolean literals. [3] (This is also the common Alembic/SQLAlchemy migration pattern you’ll see recommended in practice. [4])

Sources: [1] SQLAlchemy migration note on string server_default quoting behavior (docs.sqlalchemy.org); [2] SQLAlchemy defaults docs recommending text() for literal SQL in server_default (docs.sqlalchemy.org); [3] SQLAlchemy false() constant portability across backends (docs.sqlalchemy.org); [4] Alembic/SQLAlchemy usage example recommending sa.false() (stackoverflow.com).

Citations:


Use sa.false() for the kerberos_conn server default.

server_default='false' works, but sa.false() is the recommended portable form in Alembic/SQLAlchemy. It ensures the default renders as a proper SQL boolean literal across different database backends (including SQLite) rather than relying on string-to-boolean coercion.

🔧 Suggested fix
             ('kerberos_conn',
              sa.Column('kerberos_conn', sa.Boolean(),
                        nullable=False,
-                       server_default='false')),
+                       server_default=sa.false())),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
('kerberos_conn',
sa.Column('kerberos_conn', sa.Boolean(),
nullable=False,
server_default='false')),
('kerberos_conn',
sa.Column('kerberos_conn', sa.Boolean(),
nullable=False,
server_default=sa.false())),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/migrations/versions/add_user_id_to_debugger_func_args_.py` around lines
161 - 164, The migration defines the 'kerberos_conn' column with
server_default='false'; change this to use SQLAlchemy's portable boolean literal
by setting server_default=sa.false() on the sa.Column(...) for 'kerberos_conn'
(and ensure sa.false is available from the imported sa namespace if not
already). This ensures the default renders as a proper SQL boolean literal
across DB backends.

Comment on lines +230 to +231
server.kerberos_conn = sharedserver.kerberos_conn
server.tags = sharedserver.tags
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 | 🟠 Major

Use sharedserver.tags as the merge base for non-owner updates.

Now that tags is overlaid from SharedServer, update() still builds tag deltas from server.tags on the owner row. Editing an existing shared copy can therefore rebuild the list from the owner's tags and overwrite the non-owner's current SharedServer.tags.

🔧 Suggested fix
-        self.update_tags(data, server)
+        self.update_tags(data, sharedserver or server)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/browser/server_groups/servers/__init__.py` around lines 230 -
231, The update path currently computes tag deltas using server.tags (the
owner's tags) which causes non-owner SharedServer.tags to be rebuilt from the
owner; change the merge base so that when handling a non-owner update (where
SharedServer exists and you assign server.kerberos_conn =
sharedserver.kerberos_conn and server.tags = sharedserver.tags) you compute tag
diffs against sharedserver.tags (i.e. use SharedServer.tags as the base) instead
of server.tags inside update(), and apply only the delta to the non-owner
SharedServer record to avoid overwriting its existing tags.

Comment on lines 1182 to +1184
'passexec_expiration':
server.passexec_expiration
if server.passexec_expiration and
not _is_non_owner(server) else None,
if server.passexec_expiration else None,
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

Keep 0 distinct from None for passexec_expiration.

This truthiness check collapses an explicit 0 to None, so that value will not round-trip through the properties API.

🔧 Suggested fix
-            'passexec_expiration':
-                server.passexec_expiration
-                if server.passexec_expiration else None,
+            'passexec_expiration': server.passexec_expiration,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'passexec_expiration':
server.passexec_expiration
if server.passexec_expiration and
not _is_non_owner(server) else None,
if server.passexec_expiration else None,
'passexec_expiration': server.passexec_expiration,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/browser/server_groups/servers/__init__.py` around lines 1182 -
1184, The current truthiness check for passexec_expiration collapses an explicit
0 to None; change the conditional to test explicitly for None (e.g. use "if
server.passexec_expiration is not None else None") so that a value of 0 is
preserved when building the properties dict for passexec_expiration and will
round-trip correctly through the properties API.

Comment on lines +563 to +569
passexec_cmd = db.Column(db.Text(), nullable=True)
passexec_expiration = db.Column(db.Integer(), nullable=True)
kerberos_conn = db.Column(
db.Boolean(), nullable=False, default=0
)
tags = db.Column(types.JSON)
post_connection_sql = db.Column(db.String(), nullable=True)
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 | 🟠 Major

Bump SCHEMA_VERSION with these new SharedServer columns.

This changes the config DB schema, but SCHEMA_VERSION is still 50 in this module. Existing installs that gate upgrades off that version can miss the migration and keep running against a sharedserver table that does not have these fields yet.

🔧 Suggested fix
-SCHEMA_VERSION = 50
+SCHEMA_VERSION = 51
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/model/__init__.py` around lines 563 - 569, The SCHEMA_VERSION
constant was not incremented after adding new SharedServer columns
(passexec_cmd, passexec_expiration, kerberos_conn, tags, post_connection_sql),
so update the module's SCHEMA_VERSION (e.g., from 50 to 51) where it is defined
so upgrades detect and run the migration for the SharedServer changes; ensure
the new version is used by migration/upgrade logic and any tests referencing
SCHEMA_VERSION are updated accordingly.

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