diff --git a/docs/en_US/images/server_advanced.png b/docs/en_US/images/server_advanced.png index 9ab369fb1..647d88791 100644 Binary files a/docs/en_US/images/server_advanced.png and b/docs/en_US/images/server_advanced.png differ diff --git a/docs/en_US/images/server_connection.png b/docs/en_US/images/server_connection.png index d2505b36d..4229a853e 100644 Binary files a/docs/en_US/images/server_connection.png and b/docs/en_US/images/server_connection.png differ diff --git a/docs/en_US/images/server_general.png b/docs/en_US/images/server_general.png index 48a1b4568..7aa35c00b 100644 Binary files a/docs/en_US/images/server_general.png and b/docs/en_US/images/server_general.png differ diff --git a/docs/en_US/images/server_parameters.png b/docs/en_US/images/server_parameters.png new file mode 100644 index 000000000..e9f5868e0 Binary files /dev/null and b/docs/en_US/images/server_parameters.png differ diff --git a/docs/en_US/images/server_ssh_tunnel.png b/docs/en_US/images/server_ssh_tunnel.png index d4e7b68a6..c3d3fd6a4 100644 Binary files a/docs/en_US/images/server_ssh_tunnel.png and b/docs/en_US/images/server_ssh_tunnel.png differ diff --git a/docs/en_US/images/server_ssl.png b/docs/en_US/images/server_ssl.png deleted file mode 100644 index 13ba3dea4..000000000 Binary files a/docs/en_US/images/server_ssl.png and /dev/null differ diff --git a/docs/en_US/release_notes_6_20.rst b/docs/en_US/release_notes_6_20.rst index 66ccfc372..6cfaf9477 100644 --- a/docs/en_US/release_notes_6_20.rst +++ b/docs/en_US/release_notes_6_20.rst @@ -15,10 +15,12 @@ Supported Database Servers New features ************ + | `Issue #4728 `_ - Added support for setting PostgreSQL connection parameters. Housekeeping ************ + | `Issue #5525 `_ - Upgrade Flask-Migrate to 4.x. | `Issue #5723 `_ - Improve performance by removing signal-based zoom-in, zoom-out, etc functionality from the runtime environment. Bug fixes @@ -26,4 +28,6 @@ Bug fixes | `Issue #5567 `_ - Fix orphan database connections resulting in an inability to connect to databases. | `Issue #5705 `_ - Ensure that all parts of the application recommend and enforce the same length of passwords. + | `Issue #5732 `_ - Fixed an issue where Kerberos authentication to the server is not imported/exported. | `Issue #5751 `_ - Fix failing import servers CLI due to vulnerability fix. + | `Issue #5746 `_ - Increase the length of the value column of the setting table. diff --git a/docs/en_US/server_dialog.rst b/docs/en_US/server_dialog.rst index cf1a94546..d1bffd766 100644 --- a/docs/en_US/server_dialog.rst +++ b/docs/en_US/server_dialog.rst @@ -82,17 +82,30 @@ Use the fields in the *Connection* tab to configure a connection: see `Section 33.16 of the Postgres documentation `_. -Click the *SSL* tab to continue. +Click the *Parameters* tab to continue. -.. image:: images/server_ssl.png +.. image:: images/server_parameters.png :alt: Server dialog ssl tab :align: center -Use the fields in the *SSL* tab to configure SSL: +Use the fields in the *Parameters* tab to configure a connection: -* Use the drop-down list box in the *SSL* field to select the type of SSL - connection the server should use. For more information about using SSL - encryption, see +Click on the *+* button to add a new parameter. Some of the parameters are: + +* *Host address* using this field to specify the host IP address may save time + by avoiding a DNS lookup on connection, but it may be useful to specify both + a host name and address when using Kerberos, GSSAPI, or SSPI authentication + methods, as well as for verify-full SSL certificate verification. +* *Password File* field to specify the location of a password file + (.pgpass). A .pgpass file allows a user to login without providing a password + when they connect. For more information, see + `Section 33.15 of the Postgres documentation `_. +* *Connection timeout* field to specify the maximum wait for connection, + in seconds. Zero or not specified means wait indefinitely. It is not + recommended to use a timeout of less than 2 seconds. By default it is set to + 10 seconds. +* *SSL mode* field to select the type of SSL connection the server should use. + For more information about using SSL encryption, see `Section 33.18 of the Postgres documentation `_. If pgAdmin is installed in Server mode (the default mode), you can use the @@ -100,27 +113,27 @@ platform-specific File manager dialog to upload files that support SSL encryption to the server. To access the File manager dialog, click the icon that is located to the right of each of the following fields. -* Use the *Client certificate* field to specify the file containing the client +* *Client certificate* field to specify the file containing the client SSL certificate. This file will replace the default *~/.postgresql/postgresql.crt* if pgAdmin is installed in Desktop mode, and *//.postgresql/postgresql.crt* if pgAdmin is installed in Web mode. This parameter is ignored if an SSL connection is not made. -* Use the *Client certificate key* field to specify the file containing the +* *Client certificate key* field to specify the file containing the secret key used for the client certificate. This file will replace the default *~/.postgresql/postgresql.key* if pgAdmin is installed in Desktop mode, and *//.postgresql/postgresql.key* if pgAdmin is installed in Web mode. This parameter is ignored if an SSL connection is not made. -* Use the *Root certificate* field to specify the file containing the SSL +* *Root certificate* field to specify the file containing the SSL certificate authority. This file will replace the default *~/.postgresql/root.crt*. This parameter is ignored if an SSL connection is not made. -* Use the *Certificate revocation list* field to specify the file containing +* *Certificate revocation list* field to specify the file containing the SSL certificate revocation list. This list will replace the default list, found in *~/.postgresql/root.crl*. This parameter is ignored if an SSL connection is not made. -* When *SSL compression?* is set to *True*, data sent over SSL connections will - be compressed. The default value is *False* (compression is disabled). This +* *SSL compression?* is set to *True*, data sent over SSL connections will + be compressed. The default value is *False* (compression is disabled). This parameter is ignored if an SSL connection is not made. .. warning:: In Server mode, certificates, private keys, and the revocation list @@ -175,20 +188,11 @@ Click the *Advanced* tab to continue. Use the fields in the *Advanced* tab to configure a connection: -* Specify the IP address of the server host in the *Host address* field. Using - this field to specify the host IP address may save time by avoiding a DNS - lookup on connection, but it may be useful to specify both a host name and - address when using Kerberos, GSSAPI, or SSPI authentication methods, as well - as for verify-full SSL certificate verification. * Use the *DB restriction* field to provide a SQL restriction that will be used against the pg_database table to limit the databases that you see. For example, you might enter: *live_db test_db* so that only live_db and test_db are shown in the pgAdmin browser. Separate entries with a comma or tab as you type. -* Use the *Password File* field to specify the location of a password file - (.pgpass). A .pgpass file allows a user to login without providing a password - when they connect. For more information, see - `Section 33.15 of the Postgres documentation `_. * Use the *Password exec command* field to specify a shell command to be executed to retrieve a password to be used for SQL authentication. The ``stdout`` of the command will be used as the SQL password. This may be useful when the password @@ -199,10 +203,6 @@ Use the fields in the *Advanced* tab to configure a connection: the password will not expire until your pgAdmin session does. Zero means the command will be executed for each new connection or reconnection that is made. If the generated password is not valid indefinitely, set this value to slightly before it will expire. -* Use the *Connection timeout* field to specify the maximum wait for connection, - in seconds. Zero or not specified means wait indefinitely. It is not - recommended to use a timeout of less than 2 seconds. By default it is set to - 10 seconds. .. note:: The password file option is only supported when pgAdmin is using libpq v10.0 or later to connect to the server. diff --git a/requirements.txt b/requirements.txt index 5a7a25ff6..28696cb08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ Flask==2.1.*; python_version >= '3.7' Flask-Gravatar==0.* Flask-Login==0.* Flask-Mail==0.* -Flask-Migrate==3.* +Flask-Migrate==4.* dnspython==2.2.1 greenlet==1.1.2; python_version <= '3.10' Flask-SQLAlchemy==2.5.* diff --git a/web/migrations/versions/f656e56dfdc8_.py b/web/migrations/versions/f656e56dfdc8_.py new file mode 100644 index 000000000..24478fd08 --- /dev/null +++ b/web/migrations/versions/f656e56dfdc8_.py @@ -0,0 +1,99 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2023, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +""" Used for connection parameter changes in the server table. + +Revision ID: f656e56dfdc8 +Revises: f79844e926ae +Create Date: 2023-01-02 14:52:48.109290 + +""" +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = 'f656e56dfdc8' +down_revision = 'f79844e926ae' +branch_labels = None +depends_on = None + + +def migrate_connection_params(table_name): + """ + This function is used to add connection parameter as JSON data and drop + unused columns. + """ + op.add_column(table_name, + sa.Column('connection_params', sa.JSON())) + + # define table representation + meta = sa.MetaData(bind=op.get_bind()) + meta.reflect(only=(table_name,)) + server_table = sa.Table(table_name, meta) + + # Create a select statement + stmt = sa.select([ + server_table.columns.id, server_table.columns.ssl_mode, + server_table.columns.sslcert, server_table.columns.sslkey, + server_table.columns.sslrootcert, server_table.columns.sslcrl, + server_table.columns.sslcompression, server_table.columns.hostaddr, + server_table.columns.passfile, server_table.columns.connect_timeout + ]) + + # Fetch the data from the server table + results = op.get_bind().execute(stmt).fetchall() + for rows in results: + connection_params = {} + server_id = 0 + for key, value in rows.items(): + if key == 'id': + server_id = value + # Name is changed from ssl_mode to sslmode + if key == 'ssl_mode': + key = 'sslmode' + + if value is not None and key != 'id': + connection_params[key] = value + + # Update the newly added column with JSON data. + op.execute( + server_table.update().where(server_table.columns.id == server_id) + .values(connection_params=connection_params) + ) + + # Drop unused columns + with op.batch_alter_table(table_name) as batch_op: + if table_name == 'server': + batch_op.drop_constraint('ck_ssl_mode') + batch_op.drop_column('ssl_mode') + batch_op.drop_column('sslcert') + batch_op.drop_column('sslkey') + batch_op.drop_column('sslrootcert') + batch_op.drop_column('sslcrl') + batch_op.drop_column('sslcompression') + batch_op.drop_column('hostaddr') + batch_op.drop_column('passfile') + batch_op.drop_column('connect_timeout') + + +def upgrade(): + migrate_connection_params('server') + migrate_connection_params('sharedserver') + + # Increasing the length of the value column of the setting table. + with op.batch_alter_table("setting") as batch_op: + batch_op.alter_column('value', + existing_type=sa.String(length=1024), + type_=sa.String(length=2048), + existing_nullable=False) + + +def downgrade(): + # pgAdmin only upgrades, downgrade not implemented. + pass diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index b1911b9c8..9dadab767 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -608,7 +608,8 @@ def create_app(app_name=None): port=port, maintenance_db='postgres', username=superuser, - ssl_mode='prefer', + connection_params={'sslmode': 'prefer', + 'connect_timeout': 10}, comment=comment, discovery_id=discovery_id) diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 52fbeb576..790a310ff 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -157,9 +157,19 @@ class ServerModule(sg.ServerGroupPluginModule): server.tunnel_username = sharedserver.tunnel_username server.tunnel_password = sharedserver.tunnel_password server.save_password = sharedserver.save_password - server.passfile = sharedserver.passfile + if hasattr(server, 'connection_params') and \ + hasattr(sharedserver, 'connection_params') and \ + 'passfile' in server.connection_params and \ + 'passfile' in sharedserver.connection_params: + server.connection_params['passfile'] = \ + sharedserver.connection_params['passfile'] server.servergroup_id = sharedserver.servergroup_id - server.sslcert = sharedserver.sslcert + if hasattr(server, 'connection_params') and \ + hasattr(sharedserver, 'connection_params') and \ + 'sslcert' in server.connection_params and \ + 'sslcert' in sharedserver.connection_params: + server.connection_params['sslcert'] = \ + sharedserver.connection_params['sslcert'] server.username = sharedserver.username server.server_owner = sharedserver.server_owner server.password = sharedserver.password @@ -346,29 +356,23 @@ class ServerModule(sg.ServerGroupPluginModule): servergroup_id=gid, name=data.name, host=data.host, - hostaddr=data.hostaddr, port=data.port, maintenance_db=data.maintenance_db, username=None, save_password=0, - ssl_mode=data.ssl_mode, comment=None, role=data.role, - sslcert=None, - sslkey=None, - sslrootcert=None, - sslcrl=None, bgcolor=data.bgcolor if data.bgcolor else None, fgcolor=data.fgcolor if data.fgcolor else None, service=data.service if data.service else None, - connect_timeout=0, use_ssh_tunnel=data.use_ssh_tunnel, tunnel_host=data.tunnel_host, tunnel_port=22, tunnel_username=None, tunnel_authentication=0, tunnel_identity_file=None, - shared=True + shared=True, + connection_params=data.connection_params ) db.session.add(shared_server) db.session.commit() @@ -486,11 +490,53 @@ class ServerNode(PGChildNodeView): ) data[field] = dummy_ssl_file # For Desktop mode, we will allow to default - else: - data[field] = None return flag, data + def convert_connection_parameter(self, params): + """ + This function is used to convert the connection parameter based + on the instance type. + """ + conn_params = None + # if params is of type list then it is coming from the frontend, + # and we have to convert it into the dict and store it into the + # database + if isinstance(params, list): + conn_params = {} + for item in params: + conn_params[item['name']] = item['value'] + # if params is of type dict then it is coming from the database, + # and we have to convert it into the list of params to show on GUI. + elif isinstance(params, dict): + conn_params = [] + for key, value in params.items(): + if value is not None: + conn_params.append( + {'name': key, 'keyword': key, 'value': value}) + + return conn_params + + def update_connection_parameter(self, data, server): + """ + This function is used to update the connection parameters. + """ + if 'connection_params' in data and \ + hasattr(server, 'connection_params'): + existing_conn_params = getattr(server, 'connection_params') + new_conn_params = data['connection_params'] + if 'deleted' in new_conn_params: + for item in new_conn_params['deleted']: + del existing_conn_params[item['name']] + if 'added' in new_conn_params: + for item in new_conn_params['added']: + existing_conn_params[item['name']] = item['value'] + if 'changed' in new_conn_params: + for item in new_conn_params['changed']: + existing_conn_params[item['name']] = item['value'] + + data['connection_params'] = existing_conn_params + @login_required def nodes(self, gid): res = [] @@ -701,27 +747,18 @@ class ServerNode(PGChildNodeView): config_param_map = { 'name': 'name', 'host': 'host', - 'hostaddr': 'hostaddr', 'port': 'port', 'db': 'maintenance_db', 'username': 'username', - 'sslmode': 'ssl_mode', 'gid': 'servergroup_id', 'comment': 'comment', 'role': 'role', 'db_res': 'db_res', - 'passfile': 'passfile', 'passexec_cmd': 'passexec_cmd', 'passexec_expiration': 'passexec_expiration', - 'sslcert': 'sslcert', - 'sslkey': 'sslkey', - 'sslrootcert': 'sslrootcert', - 'sslcrl': 'sslcrl', - 'sslcompression': 'sslcompression', 'bgcolor': 'bgcolor', 'fgcolor': 'fgcolor', 'service': 'service', - 'connect_timeout': 'connect_timeout', 'use_ssh_tunnel': 'use_ssh_tunnel', 'tunnel_host': 'tunnel_host', 'tunnel_port': 'tunnel_port', @@ -730,15 +767,14 @@ class ServerNode(PGChildNodeView): 'tunnel_identity_file': 'tunnel_identity_file', 'shared': 'shared', 'kerberos_conn': 'kerberos_conn', + 'connection_params': 'connection_params' } disp_lbl = { 'name': gettext('name'), - 'hostaddr': gettext('Host name/address'), 'port': gettext('Port'), 'db': gettext('Maintenance database'), 'username': gettext('Username'), - 'sslmode': gettext('SSL Mode'), 'comment': gettext('Comments'), 'role': gettext('Role') } @@ -750,12 +786,16 @@ class ServerNode(PGChildNodeView): if 'db_res' in data: data['db_res'] = ','.join(data['db_res']) - hostaddr = data.get('hostaddr') - if hostaddr and not is_valid_ipaddress(hostaddr): + # Update connection parameter if any. + self.update_connection_parameter(data, server) + + if 'connection_params' in data and \ + 'hostaddr' in data['connection_params'] and \ + not is_valid_ipaddress(data['connection_params']['hostaddr']): return make_json_response( success=0, status=400, - errormsg=gettext('Host address not valid') + errormsg=gettext('Not a valid Host address') ) manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) @@ -849,8 +889,7 @@ class ServerNode(PGChildNodeView): if connected: for arg in ( - 'hostaddr', 'db', 'sslmode', - 'role', 'service' + 'db', 'role', 'service' ): if arg in data: return forbidden( @@ -911,10 +950,6 @@ class ServerNode(PGChildNodeView): def properties(self, gid, sid): """Return list of attributes of a server""" - sslcert = None - sslkey = None - sslrootcert = None - sslcrl = None server = Server.query.filter_by( id=sid).first() @@ -941,19 +976,13 @@ class ServerNode(PGChildNodeView): shared_server) server_owner = server.server_owner - is_ssl = True if server.ssl_mode in self.SSL_MODES else False - - if is_ssl: - sslcert = server.sslcert - sslkey = server.sslkey - sslrootcert = server.sslrootcert - sslcrl = server.sslcrl - use_ssh_tunnel = 0 tunnel_host = None tunnel_port = 22 tunnel_username = None tunnel_authentication = 0 + connection_params = \ + self.convert_connection_parameter(server.connection_params) if server.use_ssh_tunnel: use_ssh_tunnel = server.use_ssh_tunnel @@ -968,7 +997,6 @@ class ServerNode(PGChildNodeView): 'server_owner': server_owner, 'user_id': server.user_id, 'host': server.host, - 'hostaddr': server.hostaddr, 'port': server.port, 'db': server.maintenance_db, 'shared': server.shared if config.SERVER_MODE else None, @@ -979,26 +1007,16 @@ class ServerNode(PGChildNodeView): 'role': server.role, 'connected': connected, 'version': manager.ver, - 'sslmode': server.ssl_mode, 'server_type': manager.server_type if connected else 'pg', 'bgcolor': server.bgcolor, 'fgcolor': server.fgcolor, 'db_res': server.db_res.split(',') if server.db_res else None, - 'passfile': server.passfile if server.passfile else None, 'passexec_cmd': server.passexec_cmd if server.passexec_cmd else None, 'passexec_expiration': server.passexec_expiration if server.passexec_expiration else None, - 'sslcert': sslcert, - 'sslkey': sslkey, - 'sslrootcert': sslrootcert, - 'sslcrl': sslcrl, - 'sslcompression': True if is_ssl and server.sslcompression - else False, 'service': server.service if server.service else None, - 'connect_timeout': - server.connect_timeout if server.connect_timeout else 0, 'use_ssh_tunnel': use_ssh_tunnel, 'tunnel_host': tunnel_host, 'tunnel_port': tunnel_port, @@ -1009,7 +1027,9 @@ class ServerNode(PGChildNodeView): 'kerberos_conn': bool(server.kerberos_conn), 'gss_authenticated': manager.gss_authenticated, 'gss_encrypted': manager.gss_encrypted, - 'cloud_status': server.cloud_status + 'cloud_status': server.cloud_status, + 'connection_params': connection_params, + 'connection_string': manager.connection_string } return ajax_response(response) @@ -1017,11 +1037,7 @@ class ServerNode(PGChildNodeView): @login_required def create(self, gid): """Add a server node to the settings database""" - required_args = [ - 'name', - 'db', - 'sslmode', - ] + required_args = ['name', 'db'] data = request.form if request.form else json.loads( request.data, encoding='utf-8' @@ -1057,8 +1073,11 @@ class ServerNode(PGChildNodeView): ).format(arg) ) - hostaddr = data.get('hostaddr') - if hostaddr and not is_valid_ipaddress(data['hostaddr']): + connection_params = self.convert_connection_parameter( + data.get('connection_params', [])) + + if 'hostaddr' in connection_params and \ + not is_valid_ipaddress(connection_params['hostaddr']): return make_json_response( success=0, status=400, @@ -1066,7 +1085,10 @@ class ServerNode(PGChildNodeView): ) # To check ssl configuration - is_ssl, data = self.check_ssl_fields(data) + is_ssl, connection_params = self.check_ssl_fields(connection_params) + # set the connection params again in the data + if 'connection_params' in data: + data['connection_params'] = connection_params server = None @@ -1076,26 +1098,18 @@ class ServerNode(PGChildNodeView): servergroup_id=data.get('gid', gid), name=data.get('name'), host=data.get('host', None), - hostaddr=hostaddr, port=data.get('port'), maintenance_db=data.get('db', None), username=data.get('username'), save_password=1 if data.get('save_password', False) and config.ALLOW_SAVE_PASSWORD else 0, - ssl_mode=data.get('sslmode'), comment=data.get('comment', None), role=data.get('role', None), db_res=','.join(data['db_res']) if 'db_res' in data else None, - sslcert=data.get('sslcert', None), - sslkey=data.get('sslkey', None), - sslrootcert=data.get('sslrootcert', None), - sslcrl=data.get('sslcrl', None), - sslcompression=1 if is_ssl and data['sslcompression'] else 0, bgcolor=data.get('bgcolor', None), fgcolor=data.get('fgcolor', None), service=data.get('service', None), - connect_timeout=data.get('connect_timeout', 0), use_ssh_tunnel=1 if data.get('use_ssh_tunnel', False) else 0, tunnel_host=data.get('tunnel_host', None), tunnel_port=data.get('tunnel_port', 22), @@ -1104,10 +1118,10 @@ class ServerNode(PGChildNodeView): False) else 0, tunnel_identity_file=data.get('tunnel_identity_file', None), shared=data.get('shared', None), - passfile=data.get('passfile', None), passexec_cmd=data.get('passexec_cmd', None), passexec_expiration=data.get('passexec_expiration', None), kerberos_conn=1 if data.get('kerberos_conn', False) else 0, + connection_params=connection_params ) db.session.add(server) db.session.commit() @@ -1131,10 +1145,9 @@ class ServerNode(PGChildNodeView): have_password = True password = data['password'] password = encrypt(password, crypt_key) - elif 'passfile' in data and data["passfile"] != '': + elif 'passfile' in data['connection_params'] and \ + data['connection_params']['passfile'] != '': passfile = data['passfile'] - setattr(server, 'passfile', passfile) - db.session.commit() if 'tunnel_password' in data and data["tunnel_password"] != '': have_tunnel_password = True @@ -1391,14 +1404,20 @@ class ServerNode(PGChildNodeView): return internal_server_error(errormsg=str(e)) if 'password' not in data and (server.kerberos_conn is False or server.kerberos_conn is None): + + passfile_param = None + if hasattr(server, 'connection_params') and \ + 'passfile' in server.connection_params: + passfile_param = server.connection_params['passfile'] + conn_passwd = getattr(conn, 'password', None) if conn_passwd is None and not server.save_password and \ - server.passfile is None and \ + passfile_param is None and \ server.passexec_cmd is None and \ server.service is None: prompt_password = True - elif server.passfile and server.passfile != '': - passfile = server.passfile + elif passfile_param and passfile_param != '': + passfile = passfile_param else: password = conn_passwd or server.password else: @@ -1657,8 +1676,11 @@ class ServerNode(PGChildNodeView): # If there is no password found for the server # then check for pgpass file if not server.password and not manager.password and \ - server.passfile and manager.passfile and \ - server.passfile == manager.passfile: + hasattr(server, 'connection_params') and \ + 'passfile' in server.connection_params and \ + manager.get_connection_param_value('passfile') and \ + server.connection_params['passfile'] == \ + manager.get_connection_param_value('passfile'): is_passfile = True # Check for password only if there is no pgpass file used @@ -1859,8 +1881,11 @@ class ServerNode(PGChildNodeView): ) if (not server.password or not manager.password) and \ - server.passfile and manager.passfile and \ - server.passfile == manager.passfile: + hasattr(server, 'connection_params') and \ + 'passfile' in server.connection_params and \ + manager.get_connection_param_value('passfile') and \ + server.connection_params['passfile'] == \ + manager.get_connection_param_value('passfile'): is_pgpass = True return make_json_response( success=1, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/__init__.py index f427cbb77..e1986d147 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/__init__.py @@ -2252,9 +2252,10 @@ class MViewNode(ViewNode, VacuumSettings): manager.export_password_env(p.id) # Check for connection timeout and if it is greater than 0 # then set the environment variable PGCONNECT_TIMEOUT. - if manager.connect_timeout > 0: + timeout = manager.get_connection_param_value('connect_timeout') + if timeout and timeout > 0: env = dict() - env['PGCONNECT_TIMEOUT'] = str(manager.connect_timeout) + env['PGCONNECT_TIMEOUT'] = str(timeout) p.set_env_variables(server, env=env) else: p.set_env_variables(server) diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js index fb206318b..2daf9df23 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js @@ -29,6 +29,7 @@ define('pgadmin.node.server', [ type: 'server', dialogHelp: url_for('help.static', {'filename': 'server_dialog.html'}), label: gettext('Server'), + width: pgBrowser.stdW.md + 'px', canDrop: function(node){ let serverOwner = node.user_id; return !(serverOwner != current_user.id && !_.isUndefined(serverOwner)); diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js index 3ed8e6f45..9d32d3852 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js @@ -9,15 +9,12 @@ import gettext from 'sources/gettext'; import _ from 'lodash'; -import {Address4, Address6} from 'ip-address'; - - import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import pgAdmin from 'sources/pgadmin'; import {default as supportedServers} from 'pgadmin.server.supported_servers'; - import current_user from 'pgadmin.user_management.current_user'; import { isEmptyString } from 'sources/validators'; +import VariableSchema from './variable.ui'; export default class ServerSchema extends BaseUISchema { constructor(serverGroupOptions=[], userId=0, initValues={}) { @@ -27,9 +24,7 @@ export default class ServerSchema extends BaseUISchema { name: '', bgcolor: '', fgcolor: '', - sslmode: 'prefer', host: '', - hostaddr: '', port: 5432, db: 'postgres', username: current_user.name, @@ -38,14 +33,8 @@ export default class ServerSchema extends BaseUISchema { password: undefined, save_password: false, db_res: [], - passfile: undefined, passexec: undefined, passexec_expiration: undefined, - sslcompression: false, - sslcert: undefined, - sslkey: undefined, - sslrootcert: undefined, - sslcrl: undefined, service: undefined, use_ssh_tunnel: 0, tunnel_host: undefined, @@ -55,16 +44,22 @@ export default class ServerSchema extends BaseUISchema { tunnel_password: undefined, tunnel_authentication: false, save_tunnel_password: false, - connect_timeout: 10, + connection_string: undefined, + connection_params: [ + {'name': 'sslmode', 'value': 'prefer', 'keyword': 'sslmode'}, + {'name': 'connect_timeout', 'value': 10, 'keyword': 'connect_timeout'}], ...initValues, }); this.serverGroupOptions = serverGroupOptions; + this.paramSchema = new VariableSchema(this.getConnectionParameters(), null, null, ['name', 'keyword', 'value']); this.userId = userId; - _.bindAll(this, 'isShared', 'isSSL'); + _.bindAll(this, 'isShared'); } - get SSL_MODES() { return ['prefer', 'require', 'verify-ca', 'verify-full']; } + initialise(state) { + this.paramSchema.setAllReadOnly(this.isConnected(state)); + } isShared(state) { return !this.isNew(state) && this.userId != current_user.id && state.shared; @@ -74,16 +69,6 @@ export default class ServerSchema extends BaseUISchema { return Boolean(state.connected); } - isSSL(state) { - return this.SSL_MODES.indexOf(state.sslmode) == -1; - } - - isValidLib() { - // older version of libpq do not support 'passfile' parameter in - // connect method, valid libpq must have version >= 100000 - return pgAdmin.Browser.utils.pg_libpq_version < 100000; - } - get baseFields() { let obj = this; return [ @@ -148,8 +133,10 @@ export default class ServerSchema extends BaseUISchema { { id: 'comment', label: gettext('Comments'), type: 'multiline', group: null, mode: ['properties', 'edit', 'create'], - }, - { + }, { + id: 'connection_string', label: gettext('Connection String'), type: 'multiline', + group: gettext('Connection'), mode: ['properties'], readonly: true, + }, { id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], disabled: obj.isShared, depChange: (state)=>{ @@ -228,103 +215,13 @@ export default class ServerSchema extends BaseUISchema { id: 'service', label: gettext('Service'), type: 'text', mode: ['properties', 'edit', 'create'], readonly: obj.isConnected, group: gettext('Connection'), - }, - { - id: 'sslmode', label: gettext('SSL mode'), type: 'select', group: gettext('SSL'), - controlProps: { - allowClear: false, - }, - mode: ['properties', 'edit', 'create'], disabled: obj.isConnected, - options: [ - {label: gettext('Allow'), value: 'allow'}, - {label: gettext('Prefer'), value: 'prefer'}, - {label: gettext('Require'), value: 'require'}, - {label: gettext('Disable'), value: 'disable'}, - {label: gettext('Verify-CA'), value: 'verify-ca'}, - {label: gettext('Verify-Full'), value: 'verify-full'}, - ], - }, - { - id: 'sslcert', label: gettext('Client certificate'), type: 'file', - group: gettext('SSL'), mode: ['edit', 'create'], - disabled: obj.isSSL, readonly: obj.isConnected, - controlProps: { - dialogType: 'select_file', supportedTypes: ['*'], - }, - deps: ['sslmode'], - }, - { - id: 'sslkey', label: gettext('Client certificate key'), type: 'file', - group: gettext('SSL'), mode: ['edit', 'create'], - disabled: obj.isSSL, readonly: obj.isConnected, - controlProps: { - dialogType: 'select_file', supportedTypes: ['*'], - }, - deps: ['sslmode'], - },{ - id: 'sslrootcert', label: gettext('Root certificate'), type: 'file', - group: gettext('SSL'), mode: ['edit', 'create'], - disabled: obj.isSSL, readonly: obj.isConnected, - controlProps: { - dialogType: 'select_file', supportedTypes: ['*'], - }, - deps: ['sslmode'], - },{ - id: 'sslcrl', label: gettext('Certificate revocation list'), type: 'file', - group: gettext('SSL'), mode: ['edit', 'create'], - disabled: obj.isSSL, readonly: obj.isConnected, - controlProps: { - dialogType: 'select_file', supportedTypes: ['*'], - }, - deps: ['sslmode'], - }, - { - id: 'sslcompression', label: gettext('SSL compression?'), type: 'switch', - mode: ['edit', 'create'], group: gettext('SSL'), - disabled: obj.isSSL, readonly: obj.isConnected, - deps: ['sslmode'], - }, - { - id: 'sslcert', label: gettext('Client certificate'), type: 'text', - group: gettext('SSL'), mode: ['properties'], - deps: ['sslmode'], - visible: function(state) { - let sslcert = state.sslcert; - return !_.isUndefined(sslcert) && !_.isNull(sslcert); - }, - },{ - id: 'sslkey', label: gettext('Client certificate key'), type: 'text', - group: gettext('SSL'), mode: ['properties'], - deps: ['sslmode'], - visible: function(state) { - let sslkey = state.sslkey; - return !_.isUndefined(sslkey) && !_.isNull(sslkey); - }, - },{ - id: 'sslrootcert', label: gettext('Root certificate'), type: 'text', - group: gettext('SSL'), mode: ['properties'], - deps: ['sslmode'], - visible: function(state) { - let sslrootcert = state.sslrootcert; - return !_.isUndefined(sslrootcert) && !_.isNull(sslrootcert); - }, - },{ - id: 'sslcrl', label: gettext('Certificate revocation list'), type: 'text', - group: gettext('SSL'), mode: ['properties'], - deps: ['sslmode'], - visible: function(state) { - let sslcrl = state.sslcrl; - return !_.isUndefined(sslcrl) && !_.isNull(sslcrl); - }, - },{ - id: 'sslcompression', label: gettext('SSL compression?'), type: 'switch', - mode: ['properties'], group: gettext('SSL'), - deps: ['sslmode'], - visible: function(state) { - return _.indexOf(obj.SSL_MODES, state.sslmode) != -1; - }, - }, - { + }, { + id: 'connection_params', label: gettext('Connection Parameters'), + type: 'collection', group: gettext('Parameters'), + schema: this.paramSchema, mode: ['edit', 'create'], uniqueCol: ['name'], + canAdd: (state)=> !obj.isConnected(state), canEdit: false, + canDelete: (state)=> !obj.isConnected(state), + }, { id: 'use_ssh_tunnel', label: gettext('Use SSH tunneling'), type: 'switch', mode: ['properties', 'edit', 'create'], group: gettext('SSH Tunnel'), disabled: function() { @@ -401,9 +298,6 @@ export default class ServerSchema extends BaseUISchema { disabled: function(state) { return (!current_user.allow_save_tunnel_password || !state.use_ssh_tunnel); }, - }, { - id: 'hostaddr', label: gettext('Host address'), type: 'text', group: gettext('Advanced'), - mode: ['properties', 'edit', 'create'], readonly: obj.isConnected, }, { id: 'db_res', label: gettext('DB restriction'), type: 'select', group: gettext('Advanced'), @@ -411,22 +305,6 @@ export default class ServerSchema extends BaseUISchema { mode: ['properties', 'edit', 'create'], readonly: obj.isConnected, controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: 'Specify the databases to be restrict...'}, }, - { - id: 'passfile', label: gettext('Password file'), type: 'file', - group: gettext('Advanced'), mode: ['edit', 'create'], - disabled: obj.isValidLib, readonly: obj.isConnected, - controlProps: { - dialogType: 'select_file', supportedTypes: ['*'], - }, - }, - { - id: 'passfile', label: gettext('Password file'), type: 'text', - group: gettext('Advanced'), mode: ['properties'], - visible: function(state) { - let passfile = state.passfile; - return !_.isUndefined(passfile) && !_.isNull(passfile); - }, - }, { id: 'passexec_cmd', label: gettext('Password exec command'), type: 'text', group: gettext('Advanced'), @@ -439,12 +317,6 @@ export default class ServerSchema extends BaseUISchema { visible: function(state) { return !_.isEmpty(state.passexec_cmd); }, - }, - { - id: 'connect_timeout', label: gettext('Connection timeout (seconds)'), - type: 'int', group: gettext('Advanced'), - mode: ['properties', 'edit', 'create'], readonly: obj.isConnected, - min: 0, } ]; } @@ -453,30 +325,12 @@ export default class ServerSchema extends BaseUISchema { let errmsg = null; if (isEmptyString(state.service)) { - errmsg = gettext('Either Host name, Address or Service must be specified.'); - if(isEmptyString(state.host) && isEmptyString(state.hostaddr)) { + errmsg = gettext('Either Host name or Service must be specified.'); + if(isEmptyString(state.host)) { setError('host', errmsg); return true; } else { setError('host', null); - setError('hostaddr', null); - } - - /* IP address validate */ - if (state.hostaddr) { - try { - new Address4(state.hostaddr); - } catch(e) { - try { - new Address6(state.hostaddr); - } catch(ex) { - errmsg = gettext('Host address must be valid IPv4 or IPv6 address.'); - setError('hostaddr', errmsg); - return true; - } - } - } else { - setError('hostaddr', null); } /* Hostname, IP address validate */ @@ -507,7 +361,7 @@ export default class ServerSchema extends BaseUISchema { setError('port', null); } } else { - _.each(['host', 'hostaddr', 'db', 'username', 'port'], (item) => { + _.each(['host', 'db', 'username', 'port'], (item) => { setError(item, null); }); } @@ -549,4 +403,92 @@ export default class ServerSchema extends BaseUISchema { } return false; } + + getConnectionParameters() { + return [{ + 'value': 'hostaddr', 'label': gettext('Host address'), 'vartype': 'string' + }, { + 'value': 'passfile', 'label': gettext('Password file'), 'vartype': 'file' + }, { + 'value': 'channel_binding', 'label': gettext('Channel binding'), 'vartype': 'enum', + 'enumvals': [gettext('prefer'), gettext('require'), gettext('disable')], + 'min_server_version': '13' + }, { + 'value': 'connect_timeout', 'label': gettext('Connection timeout (seconds)'), 'vartype': 'integer' + }, { + 'value': 'client_encoding', 'label': gettext('Client encoding'), 'vartype': 'string' + }, { + 'value': 'options', 'label': gettext('Options'), 'vartype': 'string' + }, { + 'value': 'application_name', 'label': gettext('Application name'), 'vartype': 'string' + }, { + 'value': 'fallback_application_name', 'label': gettext('Fallback application name'), 'vartype': 'string' + }, { + 'value': 'keepalives', 'label': gettext('Keepalives'), 'vartype': 'integer' + }, { + 'value': 'keepalives_idle', 'label': gettext('Keepalives idle (seconds)'), 'vartype': 'integer' + }, { + 'value': 'keepalives_interval', 'label': gettext('Keepalives interval (seconds)'), 'vartype': 'integer' + }, { + 'value': 'keepalives_count', 'label': gettext('Keepalives count'), 'vartype': 'integer' + }, { + 'value': 'tcp_user_timeout', 'label': gettext('TCP user timeout (milliseconds)'), 'vartype': 'integer', + 'min_server_version': '12' + }, { + 'value': 'tty', 'label': gettext('TTY'), 'vartype': 'string', + 'max_server_version': '13' + }, { + 'value': 'replication', 'label': gettext('Replication'), 'vartype': 'enum', + 'enumvals': [gettext('on'), gettext('off'), gettext('database')], + 'min_server_version': '11' + }, { + 'value': 'gssencmode', 'label': gettext('GSS encmode'), 'vartype': 'enum', + 'enumvals': [gettext('prefer'), gettext('require'), gettext('disable')], + 'min_server_version': '12' + }, { + 'value': 'sslmode', 'label': gettext('SSL mode'), 'vartype': 'enum', + 'enumvals': [gettext('allow'), gettext('prefer'), gettext('require'), + gettext('disable'), gettext('verify-ca'), gettext('verify-full')] + }, { + 'value': 'sslcompression', 'label': gettext('SSL compression?'), 'vartype': 'bool', + }, { + 'value': 'sslcert', 'label': gettext('Client certificate'), 'vartype': 'file' + }, { + 'value': 'sslkey', 'label': gettext('Client certificate key'), 'vartype': 'file' + }, { + 'value': 'sslpassword', 'label': gettext('SSL password'), 'vartype': 'string', + 'min_server_version': '13' + }, { + 'value': 'sslrootcert', 'label': gettext('Root certificate'), 'vartype': 'file' + }, { + 'value': 'sslcrl', 'label': gettext('Certificate revocation list'), 'vartype': 'file', + }, { + 'value': 'sslcrldir', 'label': gettext('Certificate revocation list directory'), 'vartype': 'file', + 'min_server_version': '14' + }, { + 'value': 'sslsni', 'label': gettext('Server name indication'), 'vartype': 'bool', + 'min_server_version': '14' + }, { + 'value': 'requirepeer', 'label': gettext('Require peer'), 'vartype': 'string', + }, { + 'value': 'ssl_min_protocol_version', 'label': gettext('SSL min protocol version'), + 'vartype': 'enum', 'min_server_version': '13', + 'enumvals': [gettext('TLSv1'), gettext('TLSv1.1'), gettext('TLSv1.2'), + gettext('TLSv1.3')] + }, { + 'value': 'ssl_max_protocol_version', 'label': gettext('SSL max protocol version'), + 'vartype': 'enum', 'min_server_version': '13', + 'enumvals': [gettext('TLSv1'), gettext('TLSv1.1'), gettext('TLSv1.2'), + gettext('TLSv1.3')] + }, { + 'value': 'krbsrvname', 'label': gettext('Kerberos service name'), 'vartype': 'string', + }, { + 'value': 'gsslib', 'label': gettext('GSS library'), 'vartype': 'string', + }, { + 'value': 'target_session_attrs', 'label': gettext('Target session attribute'), + 'vartype': 'enum', + 'enumvals': [gettext('any'), gettext('read-write'), gettext('read-only'), + gettext('primary'), gettext('standby'), gettext('prefer-standby')] + }]; + } } diff --git a/web/pgadmin/browser/server_groups/servers/static/js/variable.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/variable.ui.js index 6faf513df..071c3e993 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/variable.ui.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/variable.ui.js @@ -51,12 +51,18 @@ export default class VariableSchema extends BaseUISchema { value: undefined, role: null, database: null, + keyword: null, }); this.vnameOptions = vnameOptions; this.databaseOptions = databaseOptions; this.roleOptions = roleOptions; this.varTypes = {}; this.keys = keys; + this.allReadOnly = false; + } + + setAllReadOnly(isReadOnly) { + this.allReadOnly = isReadOnly; } setVarTypes(options) { @@ -67,6 +73,19 @@ export default class VariableSchema extends BaseUISchema { }); } + getPlaceHolderMsg(variable) { + let msg = ''; + if (variable?.min_server_version && variable?.max_server_version) { + msg = gettext('%s <= Supported version >= %s', variable?.max_server_version, variable?.min_server_version); + } else if (variable?.min_server_version) { + msg = gettext('Supported version >= %s', variable?.min_server_version); + } else if (variable?.max_server_version) { + msg = gettext('Supported version <= %s', variable?.max_server_version); + } + + return msg; + } + getValueFieldProps(variable) { switch(variable?.vartype) { case 'bool': @@ -74,17 +93,44 @@ export default class VariableSchema extends BaseUISchema { case 'enum': return { cell: 'select', - options: (variable.enumvals || []).map((val)=>({ + options: (variable.enumvals || []).map((val)=>(typeof(val)=='string' ? { label: val, value: val - })) + }: val)), + controlProps: { + placeholder: this.getPlaceHolderMsg(variable) + } }; case 'integer': - return 'int'; + return { + cell: 'int', + controlProps: { + placeholder: this.getPlaceHolderMsg(variable) + } + }; case 'real': - return 'numeric'; + return { + cell: 'numeric', + controlProps: { + placeholder: this.getPlaceHolderMsg(variable) + } + }; case 'string': - return 'text'; + return { + cell: 'text', + controlProps: { + placeholder: this.getPlaceHolderMsg(variable) + } + }; + case 'file': + return { + cell: 'file', + controlProps: { + dialogType: 'select_file', + supportedTypes: ['*'], + placeholder: this.getPlaceHolderMsg(variable) + } + }; default: return ''; } @@ -99,8 +145,8 @@ export default class VariableSchema extends BaseUISchema { }, { id: 'name', label: gettext('Name'), type:'text', - readonly: function(state) { - return !obj.isNew(state); + editable: function(state) { + return obj.isNew(state) || !obj.allReadOnly; }, cell: ()=>({ cell: 'select', @@ -109,9 +155,16 @@ export default class VariableSchema extends BaseUISchema { controlProps: { allowClear: false }, }), }, + { + id: 'keyword', label: gettext('Keyword'), type: '', cell: '', + deps: ['name'], minWidth: 25, + depChange: (state, source, topState, actionObj)=>{ + return { keyword: actionObj.value }; + } + }, { id: 'value', label: gettext('Value'), type: 'text', - deps: ['name'], + deps: ['name'], editable: !obj.allReadOnly, depChange: (state, source)=>{ if(source[source.length-1] == 'name') { let variable = this.varTypes[state.name]; diff --git a/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json b/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json index 9a1262d34..8b36e672c 100644 --- a/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json +++ b/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json @@ -207,21 +207,6 @@ "status_code": 200 } }, - { - "name": "Add server with advanced properties", - "url": "/browser/server/obj/", - "is_positive_test": true, - "owner_server": true, - "test_data": { - "passfile": "test.pgpass", - "hostaddr": "127.0.0.1" - }, - "mocking_required": false, - "mock_data": {}, - "expected_data": { - "status_code": 200 - } - }, { "name": "Add server with background/foreground color", "url": "/browser/server/obj/", @@ -805,22 +790,6 @@ "status_code": 410 } }, - { - "name": "Update server with incorrect hostaddr", - "url": "/browser/server/obj/", - "is_positive_test": true, - "test_data": { - "comment": "PLACE_HOLDER", - "hostaddr": "PLACE_HOLDER", - "db_res": "PLACE_HOLDER", - "id": "PLACE_HOLDER" - }, - "mocking_required": false, - "mock_data": {}, - "expected_data": { - "status_code": 400 - } - }, { "name": "update a server , make server shared", "url": "/browser/server/obj/", @@ -938,21 +907,6 @@ "status_code": 200 } }, - { - "name": "update advanced properties of server", - "url": "/browser/server/obj/", - "is_positive_test": true, - "owner_server": true, - "test_data": { - "passfile": "test_01.pgpass", - "hostaddr": "127.0.0.1" - }, - "mocking_required": false, - "mock_data": {}, - "expected_data": { - "status_code": 200 - } - }, { "name": "remove ssl properties from server", "url": "/browser/server/obj/", @@ -972,21 +926,6 @@ "status_code": 200 } }, - { - "name": "remove advanced properties from server", - "url": "/browser/server/obj/", - "is_positive_test": true, - "owner_server": true, - "test_data": { - "passfile": "", - "hostaddr": "" - }, - "mocking_required": false, - "mock_data": {}, - "expected_data": { - "status_code": 200 - } - }, { "name": "Update server with background/foreground color", "url": "/browser/server/obj/", @@ -1046,22 +985,6 @@ "status_code": 410 } }, - { - "name": "Update server with incorrect hostaddr", - "url": "/browser/server/obj/", - "is_positive_test": true, - "test_data": { - "comment": "PLACE_HOLDER", - "hostaddr": "PLACE_HOLDER", - "db_res": "PLACE_HOLDER", - "id": "PLACE_HOLDER" - }, - "mocking_required": false, - "mock_data": {}, - "expected_data": { - "status_code": 400 - } - }, { "name": "update a server , make server shared", "url": "/browser/server/obj/", diff --git a/web/pgadmin/browser/server_groups/servers/tests/utils.py b/web/pgadmin/browser/server_groups/servers/tests/utils.py index 56d475a90..ede1f4860 100644 --- a/web/pgadmin/browser/server_groups/servers/tests/utils.py +++ b/web/pgadmin/browser/server_groups/servers/tests/utils.py @@ -20,11 +20,11 @@ def create_server(server, SERVER_GROUP): server['shared'] = False server_details = (1, SERVER_GROUP, server['name'], server['host'], server['port'], server['db'], server['username'], - server['role'], server['sslmode'], server['comment'], + server['role'], server['comment'], server['shared']) cur.execute('INSERT INTO server (user_id, servergroup_id, name, host, ' - 'port, maintenance_db, username, role, ssl_mode,' - ' comment, shared) VALUES (?,?,?,?,?,?,?,?,?,?,?)', + 'port, maintenance_db, username, role,' + ' comment, shared) VALUES (?,?,?,?,?,?,?,?,?,?)', server_details) server_id = cur.lastrowid conn.commit() diff --git a/web/pgadmin/misc/bgprocess/processes.py b/web/pgadmin/misc/bgprocess/processes.py index 3a5fdb710..d3ca55481 100644 --- a/web/pgadmin/misc/bgprocess/processes.py +++ b/web/pgadmin/misc/bgprocess/processes.py @@ -808,13 +808,22 @@ class BatchProcess(): """Set environment variables""" if server: # Set SSL related ENV variables - if server.sslcert and server.sslkey and server.sslrootcert: + if hasattr(server, 'connection_params') and \ + server.connection_params and \ + 'sslcert' in server.connection_params and \ + 'sslkey' in server.connection_params and \ + 'sslrootcert' in server.connection_params: # SSL environment variables - sslcert = get_complete_file_path(server.sslcert) - sslkey = get_complete_file_path(server.sslkey) - sslrootcert = get_complete_file_path(server.sslrootcert) + sslcert = get_complete_file_path( + server.connection_params['sslcert']) + sslkey = get_complete_file_path( + server.connection_params['sslkey']) + sslrootcert = get_complete_file_path( + server.connection_params['sslrootcert']) - self.env['PGSSLMODE'] = server.ssl_mode + self.env['PGSSLMODE'] = server.connection_params['sslmode'] \ + if hasattr(server, 'connection_params') and \ + 'sslmode' in server.connection_params else 'prefer' self.env['PGSSLCERT'] = '' if sslcert is None else sslcert self.env['PGSSLKEY'] = '' if sslkey is None else sslkey self.env['PGSSLROOTCERT'] = \ diff --git a/web/pgadmin/misc/cloud/utils/__init__.py b/web/pgadmin/misc/cloud/utils/__init__.py index dc20e6463..d387374d4 100644 --- a/web/pgadmin/misc/cloud/utils/__init__.py +++ b/web/pgadmin/misc/cloud/utils/__init__.py @@ -48,9 +48,8 @@ def _create_server(data): name=data.get('name'), maintenance_db=data.get('db'), username=data.get('username'), - ssl_mode='prefer', cloud_status=data.get('cloud_status'), - connect_timeout=30, + connection_params={'sslmode': 'prefer', 'connect_timeout': 30} ) db.session.add(server) diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index 392798d1b..540be22cd 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -20,8 +20,10 @@ things: from flask_security import UserMixin, RoleMixin from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.ext.mutable import MutableDict import sqlalchemy.types as types import uuid +import json ########################################################################## # @@ -31,7 +33,7 @@ import uuid # ########################################################################## -SCHEMA_VERSION = 34 +SCHEMA_VERSION = 35 ########################################################################## # @@ -71,6 +73,25 @@ class PgAdminDbBinaryString(types.TypeDecorator): return value +class PgAdminJSONString(types.TypeDecorator): + """ + This function is used to return a string representing a json object from + an object and vise versa. + """ + + impl = types.String + + def process_bind_param(self, value, dialect): + if value is not None: + value = json.dumps(value) + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value + + class Version(db.Model): """Version numbers for reference/upgrade purposes""" __tablename__ = 'version' @@ -149,7 +170,6 @@ class Server(db.Model): ) name = db.Column(db.String(128), nullable=False) host = db.Column(db.String(128), nullable=True) - hostaddr = db.Column(db.String(128), nullable=True) port = db.Column( db.Integer(), db.CheckConstraint('port >= 1 AND port <= 65534'), @@ -163,13 +183,6 @@ class Server(db.Model): nullable=False ) role = db.Column(db.String(64), nullable=True) - ssl_mode = db.Column( - db.String(16), - db.CheckConstraint( - "ssl_mode IN ('allow', 'prefer', 'require', 'disable', " - "'verify-ca', 'verify-full')" - ), - nullable=False) comment = db.Column(db.String(1024), nullable=True) discovery_id = db.Column(db.String(128), nullable=True) servers = db.relationship( @@ -178,22 +191,11 @@ class Server(db.Model): lazy='joined' ) db_res = db.Column(db.Text(), nullable=True) - passfile = db.Column(db.Text(), nullable=True) passexec_cmd = db.Column(db.Text(), nullable=True) passexec_expiration = db.Column(db.Integer(), nullable=True) - sslcert = db.Column(db.Text(), nullable=True) - sslkey = db.Column(db.Text(), nullable=True) - sslrootcert = db.Column(db.Text(), nullable=True) - sslcrl = db.Column(db.Text(), nullable=True) - sslcompression = db.Column( - db.Integer(), - db.CheckConstraint('sslcompression >= 0 AND sslcompression <= 1'), - nullable=False - ) bgcolor = db.Column(db.String(10), nullable=True) fgcolor = db.Column(db.String(10), nullable=True) service = db.Column(db.Text(), nullable=True) - connect_timeout = db.Column(db.Integer(), nullable=False) use_ssh_tunnel = db.Column( db.Integer(), db.CheckConstraint('use_ssh_tunnel >= 0 AND use_ssh_tunnel <= 1'), @@ -216,6 +218,7 @@ class Server(db.Model): shared = db.Column(db.Boolean(), nullable=False) kerberos_conn = db.Column(db.Boolean(), nullable=False, default=0) cloud_status = db.Column(db.Integer(), nullable=False, default=0) + connection_params = db.Column(MutableDict.as_mutable(PgAdminJSONString)) @property def serialize(self): @@ -226,35 +229,27 @@ class Server(db.Model): "servergroup_id": self.servergroup_id, "name": self.name, "host": self.host, - "hostaddr": self.hostaddr, "port": self.port, "maintenance_db": self.maintenance_db, "username": self.username, "password": self.password, "save_password": self.save_password, "role": self.role, - "ssl_mode": self.ssl_mode, "comment": self.comment, "discovery_id": self.discovery_id, "db_res": self.db_res, - "passfile": self.passfile, "passexec_cmd": self.passexec_cmd, "passexec_expiration": self.passexec_expiration, - "sslcert": self.sslcert, - "sslkey": self.sslkey, - "sslrootcert": self.sslrootcert, - "sslcrl": self.sslcrl, - "sslcompression": self.sslcompression, "bgcolor": self.bgcolor, "fgcolor": self.fgcolor, "service": self.service, - "connect_timeout": self.connect_timeout, "use_ssh_tunnel": self.use_ssh_tunnel, "tunnel_host": self.tunnel_host, "tunnel_port": self.tunnel_port, "tunnel_authentication": self.tunnel_authentication, "tunnel_identity_file": self.tunnel_identity_file, - "tunnel_password": self.tunnel_password + "tunnel_password": self.tunnel_password, + "connection_params": self.connection_params } @@ -420,7 +415,6 @@ class SharedServer(db.Model): ) name = db.Column(db.String(128), nullable=False) host = db.Column(db.String(128), nullable=True) - hostaddr = db.Column(db.String(128), nullable=True) port = db.Column( db.Integer(), nullable=True) @@ -433,13 +427,6 @@ class SharedServer(db.Model): nullable=False ) role = db.Column(db.String(64), nullable=True) - ssl_mode = db.Column( - db.String(16), - db.CheckConstraint( - "ssl_mode IN ('allow', 'prefer', 'require', 'disable', " - "'verify-ca', 'verify-full')" - ), - nullable=False) comment = db.Column(db.String(1024), nullable=True) discovery_id = db.Column(db.String(128), nullable=True) servers = db.relationship( @@ -448,20 +435,9 @@ class SharedServer(db.Model): lazy='joined' ) db_res = db.Column(db.Text(), nullable=True) - passfile = db.Column(db.Text(), nullable=True) - sslcert = db.Column(db.Text(), nullable=True) - sslkey = db.Column(db.Text(), nullable=True) - sslrootcert = db.Column(db.Text(), nullable=True) - sslcrl = db.Column(db.Text(), nullable=True) - sslcompression = db.Column( - db.Integer(), - db.CheckConstraint('sslcompression >= 0 AND sslcompression <= 1'), - nullable=False - ) bgcolor = db.Column(db.String(10), nullable=True) fgcolor = db.Column(db.String(10), nullable=True) service = db.Column(db.Text(), nullable=True) - connect_timeout = db.Column(db.Integer(), nullable=False) use_ssh_tunnel = db.Column( db.Integer(), db.CheckConstraint('use_ssh_tunnel >= 0 AND use_ssh_tunnel <= 1'), @@ -482,6 +458,7 @@ class SharedServer(db.Model): tunnel_identity_file = db.Column(db.String(64), nullable=True) tunnel_password = db.Column(PgAdminDbBinaryString()) shared = db.Column(db.Boolean(), nullable=False) + connection_params = db.Column(MutableDict.as_mutable(PgAdminJSONString)) class Macros(db.Model): diff --git a/web/pgadmin/tools/backup/__init__.py b/web/pgadmin/tools/backup/__init__.py index 6b27b38ae..404965501 100644 --- a/web/pgadmin/tools/backup/__init__.py +++ b/web/pgadmin/tools/backup/__init__.py @@ -405,9 +405,10 @@ def create_backup_objects_job(sid): manager.export_password_env(p.id) # Check for connection timeout and if it is greater than 0 then # set the environment variable PGCONNECT_TIMEOUT. - if manager.connect_timeout > 0: + timeout = manager.get_connection_param_value('connect_timeout') + if timeout and timeout > 0: env = dict() - env['PGCONNECT_TIMEOUT'] = str(manager.connect_timeout) + env['PGCONNECT_TIMEOUT'] = str(timeout) p.set_env_variables(server, env=env) else: p.set_env_variables(server) diff --git a/web/pgadmin/tools/maintenance/__init__.py b/web/pgadmin/tools/maintenance/__init__.py index 8683c91bc..4d451006a 100644 --- a/web/pgadmin/tools/maintenance/__init__.py +++ b/web/pgadmin/tools/maintenance/__init__.py @@ -251,9 +251,10 @@ def create_maintenance_job(sid, did): manager.export_password_env(p.id) # Check for connection timeout and if it is greater than 0 then # set the environment variable PGCONNECT_TIMEOUT. - if manager.connect_timeout > 0: + timeout = manager.get_connection_param_value('connect_timeout') + if timeout and timeout > 0: env = dict() - env['PGCONNECT_TIMEOUT'] = str(manager.connect_timeout) + env['PGCONNECT_TIMEOUT'] = str(timeout) p.set_env_variables(server, env=env) else: p.set_env_variables(server) diff --git a/web/pgadmin/tools/psql/__init__.py b/web/pgadmin/tools/psql/__init__.py index 5c6ef1215..9621514ed 100644 --- a/web/pgadmin/tools/psql/__init__.py +++ b/web/pgadmin/tools/psql/__init__.py @@ -377,68 +377,19 @@ def get_connection_str(psql_utility, db, manager): :param db: database name to connect specific db. :return: connection attribute list for PSQL connection. """ - conn_attr = get_conn_str_win(manager, db) + manager.export_password_env('PGPASSWORD') + db = db.replace('"', '\\"') + db = db.replace("'", "\\'") + database = db if db != '' else 'postgres' + user = underscore_unescape(manager.user) if manager.user else 'postgres' + conn_attr = manager.create_connection_string(database, user) + conn_attr_list = list() conn_attr_list.append(psql_utility) conn_attr_list.append(conn_attr) return conn_attr_list -def get_conn_str_win(manager, db): - """ - Get connection attributes for psql connection. - :param manager: - :param db: - :return: - """ - manager.export_password_env('PGPASSWORD') - db = db.replace('"', '\\"') - db = db.replace("'", "\\'") - conn_attr =\ - 'host=\'{0}\' port=\'{1}\' dbname=\'{2}\' user=\'{3}\' ' \ - 'sslmode=\'{4}\' sslcompression=\'{5}\' ' \ - ''.format( - manager.local_bind_host if manager.use_ssh_tunnel else - manager.host, - manager.local_bind_port if manager.use_ssh_tunnel else - manager.port, - db if db != '' else 'postgres', - underscore_unescape(manager.user) if manager.user else 'postgres', - manager.ssl_mode, - True if manager.sslcompression else False, - ) - - if manager.hostaddr: - conn_attr = " {0} hostaddr='{1}'".format(conn_attr, manager.hostaddr) - - if manager.passfile: - conn_attr = " {0} passfile='{1}'".format(conn_attr, - get_complete_file_path( - manager.passfile)) - - if get_complete_file_path(manager.sslcert): - conn_attr = " {0} sslcert='{1}'".format( - conn_attr, get_complete_file_path(manager.sslcert)) - - if get_complete_file_path(manager.sslkey): - conn_attr = " {0} sslkey='{1}'".format( - conn_attr, get_complete_file_path(manager.sslkey)) - - if get_complete_file_path(manager.sslrootcert): - conn_attr = " {0} sslrootcert='{1}'".format( - conn_attr, get_complete_file_path(manager.sslrootcert)) - - if get_complete_file_path(manager.sslcrl): - conn_attr = " {0} sslcrl='{1}'".format( - conn_attr, get_complete_file_path(manager.sslcrl)) - - if manager.service: - conn_attr = " {0} service='{1}'".format( - conn_attr, get_complete_file_path(manager.service)) - - return conn_attr - - def enter_key_press(data): """ Handel the Enter key press event. diff --git a/web/pgadmin/tools/restore/__init__.py b/web/pgadmin/tools/restore/__init__.py index fc2cb998f..bbbdcea01 100644 --- a/web/pgadmin/tools/restore/__init__.py +++ b/web/pgadmin/tools/restore/__init__.py @@ -382,9 +382,10 @@ def create_restore_job(sid): manager.export_password_env(p.id) # Check for connection timeout and if it is greater than 0 then # set the environment variable PGCONNECT_TIMEOUT. - if manager.connect_timeout > 0: + timeout = manager.get_connection_param_value('connect_timeout') + if timeout and timeout > 0: env = dict() - env['PGCONNECT_TIMEOUT'] = str(manager.connect_timeout) + env['PGCONNECT_TIMEOUT'] = str(timeout) p.set_env_variables(server, env=env) else: p.set_env_variables(server) diff --git a/web/pgadmin/utils/__init__.py b/web/pgadmin/utils/__init__.py index 134c47c5c..7fd1e4ebf 100644 --- a/web/pgadmin/utils/__init__.py +++ b/web/pgadmin/utils/__init__.py @@ -441,31 +441,26 @@ def dump_database_servers(output_file, selected_servers, add_value(attr_dict, "Name", server.name) add_value(attr_dict, "Group", group_name) add_value(attr_dict, "Host", server.host) - add_value(attr_dict, "HostAddr", server.hostaddr) add_value(attr_dict, "Port", server.port) add_value(attr_dict, "MaintenanceDB", server.maintenance_db) add_value(attr_dict, "Username", server.username) add_value(attr_dict, "Role", server.role) - add_value(attr_dict, "SSLMode", server.ssl_mode) add_value(attr_dict, "Comment", server.comment) add_value(attr_dict, "Shared", server.shared) add_value(attr_dict, "DBRestriction", server.db_res) - add_value(attr_dict, "PassFile", server.passfile) - add_value(attr_dict, "SSLCert", server.sslcert) - add_value(attr_dict, "SSLKey", server.sslkey) - add_value(attr_dict, "SSLRootCert", server.sslrootcert) - add_value(attr_dict, "SSLCrl", server.sslcrl) - add_value(attr_dict, "SSLCompression", server.sslcompression) add_value(attr_dict, "BGColor", server.bgcolor) add_value(attr_dict, "FGColor", server.fgcolor) add_value(attr_dict, "Service", server.service) - add_value(attr_dict, "Timeout", server.connect_timeout) add_value(attr_dict, "UseSSHTunnel", server.use_ssh_tunnel) add_value(attr_dict, "TunnelHost", server.tunnel_host) add_value(attr_dict, "TunnelPort", server.tunnel_port) add_value(attr_dict, "TunnelUsername", server.tunnel_username) add_value(attr_dict, "TunnelAuthentication", server.tunnel_authentication) + add_value(attr_dict, "KerberosAuthentication", + server.kerberos_conn), + add_value(attr_dict, "ConnectionParameters", + server.connection_params) servers_dumped = servers_dumped + 1 @@ -549,14 +544,12 @@ def validate_json_data(data, is_admin): if errmsg: return errmsg - for attrib in ("SSLMode", "MaintenanceDB"): - errmsg = check_attrib(attrib) - if errmsg: - return errmsg + errmsg = check_attrib("MaintenanceDB") + if errmsg: + return errmsg - if "Host" not in obj and "HostAddr" not in obj and not \ - is_service_attrib_available: - return gettext("'Host', 'HostAddr' or 'Service' attribute not " + if "Host" not in obj and not is_service_attrib_available: + return gettext("'Host' or 'Service' attribute not " "found for server '%s'" % server) for server in skip_servers: @@ -639,36 +632,37 @@ def load_database_servers(input_file, selected_servers, new_server.name = obj["Name"] new_server.servergroup_id = group_id new_server.user_id = user_id - new_server.ssl_mode = obj["SSLMode"] new_server.maintenance_db = obj["MaintenanceDB"] new_server.host = obj.get("Host", None) - new_server.hostaddr = obj.get("HostAddr", None) - new_server.port = obj.get("Port", None) new_server.username = obj.get("Username", None) new_server.role = obj.get("Role", None) - new_server.ssl_mode = obj["SSLMode"] - new_server.comment = obj.get("Comment", None) new_server.db_res = obj.get("DBRestriction", None) - new_server.passfile = obj.get("PassFile", None) + if 'ConnectionParameters' in obj: + new_server.connection_params = \ + obj.get("ConnectionParameters", None) + else: + # JSON file format is old before introduction of the + # connection parameters. + conn_param = dict() + for item in ['HostAddr', 'SSLMode', 'PassFile', 'SSLCert', + 'SSLKey', 'SSLRootCert', 'SSLCrl', 'Timeout', + 'SSLCompression']: + if item in obj: + key = item.lower() + if item == 'Timeout': + key = 'connect_timeout' + conn_param[key] = obj.get(item) - new_server.sslcert = obj.get("SSLCert", None) - - new_server.sslkey = obj.get("SSLKey", None) - - new_server.sslrootcert = obj.get("SSLRootCert", None) - - new_server.sslcrl = obj.get("SSLCrl", None) - - new_server.sslcompression = obj.get("SSLCompression", None) + new_server.connection_params = conn_param new_server.bgcolor = obj.get("BGColor", None) @@ -676,8 +670,6 @@ def load_database_servers(input_file, selected_servers, new_server.service = obj.get("Service", None) - new_server.connect_timeout = obj.get("Timeout", None) - new_server.use_ssh_tunnel = obj.get("UseSSHTunnel", None) new_server.tunnel_host = obj.get("TunnelHost", None) @@ -689,8 +681,9 @@ def load_database_servers(input_file, selected_servers, new_server.tunnel_authentication = \ obj.get("TunnelAuthentication", None) - new_server.shared = \ - obj.get("Shared", None) + new_server.shared = obj.get("Shared", None) + + new_server.kerberos_conn = obj.get("KerberosAuthentication", None) db.session.add(new_server) diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py index 3c52d0eb4..8c8c64485 100644 --- a/web/pgadmin/utils/driver/psycopg2/connection.py +++ b/web/pgadmin/utils/driver/psycopg2/connection.py @@ -254,8 +254,6 @@ class Connection(BaseConnection): else: return True, None - pg_conn = None - passfile = None manager = self.manager crypt_key_present, crypt_key = get_crypt_key() @@ -299,7 +297,7 @@ class Connection(BaseConnection): # we will check for pgpass file availability from connection manager # if it's present then we will use it if not password and not encpass and not passfile: - passfile = manager.passfile if manager.passfile else None + passfile = manager.get_connection_param_value('passfile') if manager.passexec: password = manager.passexec.get() @@ -315,8 +313,10 @@ class Connection(BaseConnection): os.environ['PGAPPNAME'] = '{0} - {1}'.format( config.APP_NAME, conn_id) - ssl_key = get_complete_file_path(manager.sslkey) - if ssl_key and manager.ssl_mode in \ + ssl_key = get_complete_file_path( + manager.get_connection_param_value('sslkey')) + sslmode = manager.get_connection_param_value('sslmode') + if ssl_key and sslmode in \ ['require', 'verify-ca', 'verify-full']: ssl_key_file_permission = \ int(oct(os.stat(ssl_key).st_mode)[-3:]) @@ -324,27 +324,12 @@ class Connection(BaseConnection): os.chmod(ssl_key, 0o600) with ConnectionLocker(manager.kerberos_conn): - pg_conn = psycopg2.connect( - host=manager.local_bind_host if manager.use_ssh_tunnel - else manager.host, - hostaddr=manager.local_bind_host if manager.use_ssh_tunnel - else manager.hostaddr, - port=manager.local_bind_port if manager.use_ssh_tunnel - else manager.port, - database=database, - user=user, - password=password, - async_=self.async_, - passfile=get_complete_file_path(passfile), - sslmode=manager.ssl_mode, - sslcert=get_complete_file_path(manager.sslcert), - sslkey=ssl_key, - sslrootcert=get_complete_file_path(manager.sslrootcert), - sslcrl=get_complete_file_path(manager.sslcrl), - sslcompression=True if manager.sslcompression else False, - service=manager.service, - connect_timeout=manager.connect_timeout - ) + # Create the connection string + connection_string = manager.create_connection_string( + database, user, password) + + pg_conn = psycopg2.connect(connection_string, + async_=self.async_) # If connection is asynchronous then we will have to wait # until the connection is ready to use. @@ -1413,7 +1398,6 @@ WHERE db.datname = current_database()""") def reset(self): if self.conn and self.conn.closed: self.conn = None - pg_conn = None manager = self.manager is_return, return_value, password = self._decrypt_password(manager) @@ -1422,26 +1406,11 @@ WHERE db.datname = current_database()""") try: with ConnectionLocker(manager.kerberos_conn): - pg_conn = psycopg2.connect( - host=manager.local_bind_host if manager.use_ssh_tunnel - else manager.host, - hostaddr=manager.local_bind_host if manager.use_ssh_tunnel - else manager.hostaddr, - port=manager.local_bind_port if manager.use_ssh_tunnel - else manager.port, - database=self.db, - user=manager.user, - password=password, - passfile=get_complete_file_path(manager.passfile), - sslmode=manager.ssl_mode, - sslcert=get_complete_file_path(manager.sslcert), - sslkey=get_complete_file_path(manager.sslkey), - sslrootcert=get_complete_file_path(manager.sslrootcert), - sslcrl=get_complete_file_path(manager.sslcrl), - sslcompression=True if manager.sslcompression else False, - service=manager.service, - connect_timeout=manager.connect_timeout - ) + # Create the connection string + connection_string = manager.create_connection_string( + self.db, manager.user, password) + + pg_conn = psycopg2.connect(connection_string) except psycopg2.Error as e: if e.pgerror: @@ -1725,30 +1694,12 @@ Failed to reset the connection to the server due to following error: try: with ConnectionLocker(self.manager.kerberos_conn): - pg_conn = psycopg2.connect( - host=self.manager.local_bind_host if - self.manager.use_ssh_tunnel else self.manager.host, - hostaddr=self.manager.local_bind_host if - self.manager.use_ssh_tunnel else - self.manager.hostaddr, - port=self.manager.local_bind_port if - self.manager.use_ssh_tunnel else self.manager.port, - database=self.db, - user=self.manager.user, - password=password, - passfile=get_complete_file_path(self.manager.passfile), - sslmode=self.manager.ssl_mode, - sslcert=get_complete_file_path(self.manager.sslcert), - sslkey=get_complete_file_path(self.manager.sslkey), - sslrootcert=get_complete_file_path( - self.manager.sslrootcert - ), - sslcrl=get_complete_file_path(self.manager.sslcrl), - sslcompression=True if self.manager.sslcompression - else False, - service=self.manager.service, - connect_timeout=self.manager.connect_timeout - ) + # Create the connection string + connection_string = \ + self.manager.create_connection_string( + self.db, self.manager.user, password) + + pg_conn = psycopg2.connect(connection_string) # Get the cursor and run the query cur = pg_conn.cursor() @@ -1756,7 +1707,6 @@ Failed to reset the connection to the server due to following error: # Close the connection pg_conn.close() - pg_conn = None except psycopg2.Error as e: status = False diff --git a/web/pgadmin/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py index 23329f83e..50301cd86 100644 --- a/web/pgadmin/utils/driver/psycopg2/server_manager.py +++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py @@ -49,6 +49,7 @@ class ServerManager(): self.local_bind_port = None self.tunnel_object = None self.tunnel_created = False + self.connection_string = '' self.update(server) @@ -65,7 +66,6 @@ class ServerManager(): self.sid = server.id self.host = server.host - self.hostaddr = server.hostaddr self.port = server.port self.db = server.maintenance_db self.shared = server.shared @@ -73,23 +73,14 @@ class ServerManager(): self.user = server.username self.password = server.password self.role = server.role - self.ssl_mode = server.ssl_mode self.pinged = datetime.datetime.now() self.db_info = dict() self.server_types = None self.db_res = server.db_res - self.passfile = server.passfile self.passexec = \ PasswordExec(server.passexec_cmd, server.passexec_expiration) \ if server.passexec_cmd else None - self.sslcert = server.sslcert - self.sslkey = server.sslkey - self.sslrootcert = server.sslrootcert - self.sslcrl = server.sslcrl - self.sslcompression = True if server.sslcompression else False self.service = server.service - self.connect_timeout = \ - server.connect_timeout if server.connect_timeout else 0 if config.SUPPORT_SSH_TUNNEL: self.use_ssh_tunnel = server.use_ssh_tunnel self.tunnel_host = server.tunnel_host @@ -113,6 +104,9 @@ class ServerManager(): self.kerberos_conn = server.kerberos_conn self.gss_authenticated = False self.gss_encrypted = False + self.connection_params = server.connection_params + self.connection_string = self.create_connection_string(self.db, + self.user) for con in self.connections: self.connections[con]._release() @@ -623,3 +617,53 @@ WHERE db.oid = {0}""".format(did)) self.local_bind_port = None self.tunnel_object = None self.tunnel_created = False + + def get_connection_param_value(self, param_name): + """ + This function return the value of param_name if found in the + connection parameter. + """ + value = None + if self.connection_params and param_name in self.connection_params: + value = self.connection_params[param_name] + + return value + + def create_connection_string(self, database, user, password=None): + """ + This function is used to create connection string based on the + parameters. + """ + full_connection_string = \ + 'host=\'{0}\' port=\'{1}\' dbname=\'{2}\' user=\'{3}\''.format( + self.local_bind_host if self.use_ssh_tunnel else self.host, + self.local_bind_port if self.use_ssh_tunnel else self.port, + database, user) + + # Loop through all the connection parameters set in the server dialog. + if self.connection_params and isinstance(self.connection_params, dict): + for key, value in self.connection_params.items(): + # Getting complete file path if the key is one of the below. + if key in ['passfile', 'sslcert', 'sslkey', 'sslrootcert', + 'sslcrl', 'service', 'sslcrldir']: + value = get_complete_file_path(value) + + # In case of host address need to check ssh tunnel flag. + if key == 'hostaddr': + value = self.local_bind_host if self.use_ssh_tunnel else \ + value + + full_connection_string = \ + "{0} {1}='{2}'".format(full_connection_string, key, value) + + # Password should not be visible into the connection string, so + # setting the class variable with password to 'xxxxxxx'. + if password: + self.connection_string = "{0} password='xxxxxxx'".format( + full_connection_string) + + if password: + full_connection_string = "{0} password='{1}'".format( + full_connection_string, password) + + return full_connection_string diff --git a/web/regression/javascript/schema_ui_files/server.ui.spec.js b/web/regression/javascript/schema_ui_files/server.ui.spec.js index 16ae43fd0..67db1719c 100644 --- a/web/regression/javascript/schema_ui_files/server.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/server.ui.spec.js @@ -54,14 +54,9 @@ describe('ServerSchema', ()=>{ let setError = jasmine.createSpy('setError'); schemaObj.validate(state, setError); - expect(setError).toHaveBeenCalledWith('host', 'Either Host name, Address or Service must be specified.'); - - state.hostaddr = 'incorrectip'; - schemaObj.validate(state, setError); - expect(setError).toHaveBeenCalledWith('hostaddr', 'Host address must be valid IPv4 or IPv6 address.'); + expect(setError).toHaveBeenCalledWith('host', 'Either Host name or Service must be specified.'); state.host = '127.0.0.1'; - state.hostaddr = null; schemaObj.validate(state, setError); expect(setError).toHaveBeenCalledWith('username', 'Username must be specified.'); diff --git a/web/regression/javascript/schema_ui_files/variable.ui.spec.js b/web/regression/javascript/schema_ui_files/variable.ui.spec.js index 0c94d32ad..30d2de7a7 100644 --- a/web/regression/javascript/schema_ui_files/variable.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/variable.ui.spec.js @@ -74,9 +74,18 @@ describe('VariableSchema', ()=>{ expect(schemaObj.getValueFieldProps({vartype: 'enum', enumvals: []})).toEqual(jasmine.objectContaining({ cell: 'select', })); - expect(schemaObj.getValueFieldProps({vartype: 'integer'})).toBe('int'); - expect(schemaObj.getValueFieldProps({vartype: 'real'})).toBe('numeric'); - expect(schemaObj.getValueFieldProps({vartype: 'string'})).toBe('text'); + expect(schemaObj.getValueFieldProps({vartype: 'integer'})).toEqual(jasmine.objectContaining({ + cell: 'int', + })); + expect(schemaObj.getValueFieldProps({vartype: 'real'})).toEqual(jasmine.objectContaining({ + cell: 'numeric', + })); + expect(schemaObj.getValueFieldProps({vartype: 'string'})).toEqual(jasmine.objectContaining({ + cell: 'text', + })); + expect(schemaObj.getValueFieldProps({vartype: 'file'})).toEqual(jasmine.objectContaining({ + cell: 'file', + })); expect(schemaObj.getValueFieldProps({})).toBe(''); }); diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py index ea572f908..5b6a28370 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -611,10 +611,10 @@ def create_server(server): cur = conn.cursor() server_details = (1, SERVER_GROUP, server['name'], server['host'], server['port'], server['db'], server['username'], - server['role'], server['sslmode'], server['comment']) + server['role'], server['comment']) cur.execute('INSERT INTO server (user_id, servergroup_id, name, host, ' - 'port, maintenance_db, username, role, ssl_mode,' - ' comment) VALUES (?,?,?,?,?,?,?,?,?,?)', server_details) + 'port, maintenance_db, username, role,' + ' comment) VALUES (?,?,?,?,?,?,?,?,?)', server_details) server_id = cur.lastrowid conn.commit() conn.close() @@ -776,7 +776,7 @@ def get_db_server(sid): cur = conn.cursor() server = cur.execute( 'SELECT name, host, port, maintenance_db,' - ' username, ssl_mode FROM server where id=%s' % sid + ' username FROM server where id=%s' % sid ) server = server.fetchone() if server: @@ -785,14 +785,13 @@ def get_db_server(sid): db_port = server[2] db_name = server[3] username = server[4] - ssl_mode = server[5] config_servers = test_setup.config_data['server_credentials'] # Get the db password from config file for appropriate server db_password = get_db_password(config_servers, name, host, db_port) if db_password: # Drop database connection = get_db_connection( - db_name, username, db_password, host, db_port, ssl_mode + db_name, username, db_password, host, db_port ) conn.close() return connection