-
Notifications
You must be signed in to change notification settings - Fork 852
fix: SharedServer feature parity columns and write guards #9835
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -172,16 +172,17 @@ def get_shared_server_properties(server, sharedserver): | |||||||||||||
| Return shared server properties. | ||||||||||||||
|
|
||||||||||||||
| Overlays per-user SharedServer values onto the owner's Server | ||||||||||||||
| object. Security-sensitive fields that are absent from the | ||||||||||||||
| SharedServer model (passexec_cmd, post_connection_sql) are | ||||||||||||||
| suppressed for non-owners. | ||||||||||||||
| object so each non-owner sees their own customizations. | ||||||||||||||
|
|
||||||||||||||
| The server is expunged from the SQLAlchemy session before | ||||||||||||||
| mutation so that the owner's record is never dirtied. | ||||||||||||||
| :param server: | ||||||||||||||
| :param sharedserver: | ||||||||||||||
| :return: shared server (detached) | ||||||||||||||
| """ | ||||||||||||||
| if sharedserver is None: | ||||||||||||||
| return server | ||||||||||||||
|
|
||||||||||||||
| # Detach from session so in-place mutations are never | ||||||||||||||
| # flushed back to the owner's Server row. | ||||||||||||||
| sess = object_session(server) | ||||||||||||||
|
|
@@ -224,13 +225,11 @@ def get_shared_server_properties(server, sharedserver): | |||||||||||||
| server.server_owner = sharedserver.server_owner | ||||||||||||||
| server.password = sharedserver.password | ||||||||||||||
| server.prepare_threshold = sharedserver.prepare_threshold | ||||||||||||||
|
|
||||||||||||||
| # Suppress owner-only fields that are absent from SharedServer | ||||||||||||||
| # and dangerous when inherited (privilege escalation / code | ||||||||||||||
| # execution). | ||||||||||||||
| server.passexec_cmd = None | ||||||||||||||
| server.passexec_expiration = None | ||||||||||||||
| server.post_connection_sql = None | ||||||||||||||
| server.passexec_cmd = sharedserver.passexec_cmd | ||||||||||||||
| server.passexec_expiration = sharedserver.passexec_expiration | ||||||||||||||
| server.kerberos_conn = sharedserver.kerberos_conn | ||||||||||||||
| server.tags = sharedserver.tags | ||||||||||||||
|
Comment on lines
+230
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Now that 🔧 Suggested fix- self.update_tags(data, server)
+ self.update_tags(data, sharedserver or server)🤖 Prompt for AI Agents |
||||||||||||||
| server.post_connection_sql = sharedserver.post_connection_sql | ||||||||||||||
|
|
||||||||||||||
| return server | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -477,7 +476,12 @@ def create_shared_server(data, gid): | |||||||||||||
| tunnel_prompt_password=0, | ||||||||||||||
| shared=True, | ||||||||||||||
| connection_params=safe_conn_params, | ||||||||||||||
| prepare_threshold=data.prepare_threshold | ||||||||||||||
| prepare_threshold=data.prepare_threshold, | ||||||||||||||
| passexec_cmd=None, | ||||||||||||||
| passexec_expiration=None, | ||||||||||||||
| kerberos_conn=False, | ||||||||||||||
| tags=None, | ||||||||||||||
| post_connection_sql=None | ||||||||||||||
| ) | ||||||||||||||
| db.session.add(shared_server) | ||||||||||||||
| db.session.commit() | ||||||||||||||
|
|
@@ -998,8 +1002,21 @@ def _set_valid_attr_value(self, gid, data, config_param_map, server, | |||||||||||||
| if not crypt_key_present: | ||||||||||||||
| raise CryptKeyMissing | ||||||||||||||
|
|
||||||||||||||
| # Fields that non-owners must never set on their | ||||||||||||||
| # SharedServer — they enable command/SQL execution | ||||||||||||||
| # or are owner-level concepts not on SharedServer. | ||||||||||||||
| _owner_only_fields = frozenset({ | ||||||||||||||
| 'passexec_cmd', 'passexec_expiration', | ||||||||||||||
| 'post_connection_sql', | ||||||||||||||
| 'db_res', 'db_res_type', | ||||||||||||||
| }) | ||||||||||||||
|
|
||||||||||||||
| for arg in config_param_map: | ||||||||||||||
| if arg in data: | ||||||||||||||
| # Non-owners cannot set dangerous fields. | ||||||||||||||
| if _is_non_owner(server) and \ | ||||||||||||||
| arg in _owner_only_fields: | ||||||||||||||
| continue | ||||||||||||||
| value = data[arg] | ||||||||||||||
| if arg == 'password': | ||||||||||||||
| value = encrypt(data[arg], crypt_key) | ||||||||||||||
|
|
@@ -1161,12 +1178,10 @@ def properties(self, gid, sid): | |||||||||||||
| 'db_res_type': server.db_res_type, | ||||||||||||||
| 'passexec_cmd': | ||||||||||||||
| server.passexec_cmd | ||||||||||||||
| if server.passexec_cmd and | ||||||||||||||
| not _is_non_owner(server) else None, | ||||||||||||||
| if server.passexec_cmd else None, | ||||||||||||||
| 'passexec_expiration': | ||||||||||||||
| server.passexec_expiration | ||||||||||||||
| if server.passexec_expiration and | ||||||||||||||
| not _is_non_owner(server) else None, | ||||||||||||||
| if server.passexec_expiration else None, | ||||||||||||||
|
Comment on lines
1182
to
+1184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep This truthiness check collapses an explicit 🔧 Suggested fix- 'passexec_expiration':
- server.passexec_expiration
- if server.passexec_expiration else None,
+ 'passexec_expiration': server.passexec_expiration,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| 'service': server.service if server.service else None, | ||||||||||||||
| 'use_ssh_tunnel': use_ssh_tunnel, | ||||||||||||||
| 'tunnel_host': tunnel_host, | ||||||||||||||
|
|
@@ -1186,8 +1201,7 @@ def properties(self, gid, sid): | |||||||||||||
| 'connection_string': display_connection_str, | ||||||||||||||
| 'prepare_threshold': server.prepare_threshold, | ||||||||||||||
| 'tags': tags, | ||||||||||||||
| 'post_connection_sql': server.post_connection_sql | ||||||||||||||
| if not _is_non_owner(server) else None, | ||||||||||||||
| 'post_connection_sql': server.post_connection_sql, | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| return ajax_response(response) | ||||||||||||||
|
|
@@ -1605,6 +1619,13 @@ def connect(self, gid, sid, is_qt=False, server=None): | |||||||||||||
| # the API call is not made from SQL Editor or View/Edit Data tool | ||||||||||||||
| if not manager.connection().connected() and not is_qt: | ||||||||||||||
| manager.update(server) | ||||||||||||||
| # Re-suppress owner-only fields after update() which | ||||||||||||||
| # rebuilds them from the (overlaid) server object. | ||||||||||||||
| # Belt-and-suspenders: the overlay already defaults | ||||||||||||||
| # these to None, but this guards against direct DB edits. | ||||||||||||||
| if _is_non_owner(server): | ||||||||||||||
| manager.passexec = None | ||||||||||||||
| manager.post_connection_sql = None | ||||||||||||||
| conn = manager.connection() | ||||||||||||||
|
|
||||||||||||||
| # Get enc key | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -560,6 +560,13 @@ class SharedServer(db.Model, UserScopedMixin): | |
| shared = db.Column(db.Boolean(), nullable=False) | ||
| connection_params = db.Column(MutableDict.as_mutable(types.JSON)) | ||
| prepare_threshold = db.Column(db.Integer(), nullable=True) | ||
| 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) | ||
|
Comment on lines
+563
to
+569
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bump This changes the config DB schema, but 🔧 Suggested fix-SCHEMA_VERSION = 50
+SCHEMA_VERSION = 51🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| class Macros(db.Model): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In SQLAlchemy/Alembic, for a Boolean column, doesserver_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 asDEFAULT false(unquoted), i.e. notDEFAULT '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 rendersDEFAULT false; it’s just the explicit “this is SQL text” form (SQLAlchemy docs advisetext()over raw strings for literal SQL defaults). [2]server_default=sa.false()(orsa.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_defaultquoting behavior (docs.sqlalchemy.org); [2] SQLAlchemy defaults docs recommendingtext()for literal SQL inserver_default(docs.sqlalchemy.org); [3] SQLAlchemyfalse()constant portability across backends (docs.sqlalchemy.org); [4] Alembic/SQLAlchemy usage example recommendingsa.false()(stackoverflow.com).Citations:
Use
sa.false()for thekerberos_connserver default.server_default='false'works, butsa.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
🤖 Prompt for AI Agents