Add support for the hostaddr connection parameter. This helps us play nicely with Kerberos/SSPI and friends. Fixes #2191

REL-1_X
Atul Sharma 2017-06-26 15:48:59 -04:00 committed by Dave Page
parent 3f4781cdcb
commit 15cb9fc35b
7 changed files with 149 additions and 7 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -38,3 +38,10 @@ Use the fields in the *Connection* tab to configure a connection:
* Click the *Cancel* button to exit without saving work.
* Click the *Reset* button to restore configuration parameters.
Click the *Advanced* tab to continue.
.. image:: images/server_advanced.png
Use the fields in the *Advanced* tab to configure a connection:
* Specify the IP address of the server host. Using this field to specify the host IP address will avoid a DNS lookup on connection, however 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

View File

@ -0,0 +1,35 @@
"""empty message
Revision ID: 3c1e4b6eda55
Revises: 09d53fca90c7
Create Date: 2017-06-13 17:05:30.671859
"""
import base64
import sys
from alembic import op
from pgadmin.model import db, Server
import config
import os
from pgadmin.setup import get_version
# revision identifiers, used by Alembic.
revision = '3c1e4b6eda55'
down_revision = '09d53fca90c7'
branch_labels = None
depends_on = None
def upgrade():
verison = get_version()
db.engine.execute(
'ALTER TABLE server ADD COLUMN hostaddr TEXT(1024)'
)
def downgrade():
pass

View File

@ -8,7 +8,7 @@
##########################################################################
import simplejson as json
import re
import pgadmin.browser.server_groups as sg
from flask import render_template, request, make_response, jsonify, \
current_app, url_for
@ -211,6 +211,26 @@ class ServerNode(PGChildNodeView):
'delete': 'pause_wal_replay', 'put': 'resume_wal_replay'
}]
})
EXP_IP4 = "^\s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\."\
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\."\
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\."\
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\s*$"
EXP_IP6 = '^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|'\
'(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|'\
'2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|'\
'(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|'\
':((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|'\
'(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|'\
'2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|'\
'(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|'\
'[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|'\
'((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|'\
'(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|'\
'1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|'\
'((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$'
pat4 = re.compile(EXP_IP4)
pat6 = re.compile(EXP_IP6)
def nodes(self, gid):
res = []
@ -353,6 +373,7 @@ class ServerNode(PGChildNodeView):
config_param_map = {
'name': 'name',
'host': 'host',
'hostaddr': 'hostaddr',
'port': 'port',
'db': 'maintenance_db',
'username': 'username',
@ -378,13 +399,24 @@ class ServerNode(PGChildNodeView):
request.data, encoding='utf-8'
)
if 'hostaddr' in data and data['hostaddr'] != '':
if not self.pat4.match(data['hostaddr']):
if not self.pat6.match(data['hostaddr']):
return make_json_response(
success=0,
status=400,
errormsg=gettext('Host address not valid')
)
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
connected = conn.connected()
if connected:
for arg in (
'host', 'port', 'db', 'username', 'sslmode', 'role'
'host', 'hostaddr', 'port', 'db', 'username', 'sslmode', 'role'
):
if arg in data:
return forbidden(
@ -501,6 +533,7 @@ class ServerNode(PGChildNodeView):
'id': server.id,
'name': server.name,
'host': server.host,
'hostaddr': server.hostaddr,
'port': server.port,
'db': server.maintenance_db,
'username': server.username,
@ -541,6 +574,15 @@ class ServerNode(PGChildNodeView):
)
)
if 'hostaddr' in data and data['hostaddr'] != '':
if not self.pat4.match(data['hostaddr']):
if not self.pat6.match(data['hostaddr']):
return make_json_response(
success=0,
status=400,
errormsg=gettext('Host address not valid')
)
server = None
try:
@ -549,6 +591,7 @@ class ServerNode(PGChildNodeView):
servergroup_id=data[u'gid'] if u'gid' in data else gid,
name=data[u'name'],
host=data[u'host'],
hostaddr=data[u'hostaddr'] if u'hostaddr' in data else None,
port=data[u'port'],
maintenance_db=data[u'db'],
username=data[u'username'],

View File

@ -602,6 +602,7 @@ define('pgadmin.node.server', [
name: '',
sslmode: 'prefer',
host: '',
hostaddr: '',
port: 5432,
db: 'postgres',
username: current_user.name,
@ -650,6 +651,9 @@ define('pgadmin.node.server', [
},{
id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], disabled: 'isConnected'
},{
id: 'hostaddr', label: gettext('Host address'), type: 'text', group: gettext('Advanced'),
mode: ['properties', 'edit', 'create'], disabled: 'isConnected'
},{
id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], disabled: 'isConnected', min: 1024, max: 65535
@ -697,26 +701,69 @@ define('pgadmin.node.server', [
var check_for_empty = function(id, msg) {
var v = self.get(id);
if (
_.isUndefined(v) || String(v).replace(/^\s+|\s+$/g, '') == ''
_.isUndefined(v) || v === null || String(v).replace(/^\s+|\s+$/g, '') == ''
) {
err[id] = msg;
errmsg = errmsg || msg;
return true;
} else {
self.errorModel.unset(id);
return false;
}
}
var check_for_valid_ipv6 = function(val){
// Regular expression for validating IPv6 address formats
var exps = ['^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|',
'(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|',
'2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|',
'(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|',
':((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|',
'(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|',
'2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|',
'(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|',
'[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|',
'((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|',
'(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|',
'1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|',
'((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$'];
var exp = new RegExp(exps.join(''));
return exp.test(val.trim());
}
var check_for_valid_ip = function(id, msg) {
var v4exps = "(^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)";
var v4exp = new RegExp(v4exps);
var v = self.get(id);
if (
v && !(v4exp.test(v.trim()))
) {
if(!check_for_valid_ipv6(v)){
err[id] = msg;
errmsg = msg;
}
} else {
self.errorModel.unset(id);
}
}
if (!self.isNew() && 'id' in self.sessAttrs) {
err['id'] = gettext('The ID cannot be changed.');;
err['id'] = gettext('The ID cannot be changed.');
errmsg = err['id'];
} else {
self.errorModel.unset('id');
}
check_for_empty('name', gettext('Name must be specified.'));
check_for_empty(
'host', gettext('Hostname or address must be specified.')
);
if (check_for_empty(
'host', gettext('Either Host name or Host address must be specified.')
) && check_for_empty('hostaddr', gettext('Either Host name or Host address must be specified.'))){
errmsg = errmsg || gettext('Either Host name or Host address must be specified');
} else {
errmsg = undefined;
delete err['host'];
delete err['hostaddr'];
}
check_for_empty(
'db', gettext('Maintenance database must be specified.')
);
@ -724,6 +771,9 @@ define('pgadmin.node.server', [
'username', gettext('Username must be specified.')
);
check_for_empty('port', gettext('Port must be specified.'));
check_for_valid_ip(
'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
);
this.errorModel.set(err);
if (_.size(err)) {

View File

@ -108,6 +108,7 @@ class Server(db.Model):
)
name = db.Column(db.String(128), nullable=False)
host = db.Column(db.String(128), nullable=False)
hostaddr = db.Column(db.String(128), nullable=True)
port = db.Column(
db.Integer(),
db.CheckConstraint('port >= 1024 AND port <= 65534'),
@ -128,6 +129,8 @@ class Server(db.Model):
backref=db.backref('server', cascade="all, delete-orphan"),
lazy='joined')
class ModulePreference(db.Model):
"""Define a preferences table for any modules."""
__tablename__ = 'module_preference'

View File

@ -316,6 +316,7 @@ class Connection(BaseConnection):
pg_conn = psycopg2.connect(
host=mgr.host,
hostaddr=mgr.hostaddr,
port=mgr.port,
database=database,
user=user,
@ -1106,6 +1107,7 @@ Failed to execute query (execute_void) for the server #{server_id} - {conn_id}
try:
pg_conn = psycopg2.connect(
host=mgr.host,
hostaddr=mgr.hostaddr,
port=mgr.port,
database=self.db,
user=mgr.user,
@ -1373,6 +1375,7 @@ Failed to reset the connection to the server due to following error:
try:
pg_conn = psycopg2.connect(
host=self.manager.host,
hostaddr=self.manager.hostaddr,
port=self.manager.port,
database=self.db,
user=self.manager.user,
@ -1519,6 +1522,7 @@ class ServerManager(object):
self.sid = server.id
self.host = server.host
self.hostaddr = server.hostaddr
self.port = server.port
self.db = server.maintenance_db
self.did = None