Added support for IAM token based authentication for AWS RDS or Azure DB. #3491

This commit is contained in:
aelgn
2022-10-15 11:19:04 +02:00
committed by GitHub
parent 25be215180
commit a62fc2fbff
10 changed files with 176 additions and 5 deletions

View File

@@ -710,6 +710,8 @@ class ServerNode(PGChildNodeView):
'role': 'role',
'db_res': 'db_res',
'passfile': 'passfile',
'passexec_cmd': 'passexec_cmd',
'passexec_expiration': 'passexec_expiration',
'sslcert': 'sslcert',
'sslkey': 'sslkey',
'sslrootcert': 'sslrootcert',
@@ -978,6 +980,11 @@ class ServerNode(PGChildNodeView):
'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,
@@ -1092,6 +1099,8 @@ class ServerNode(PGChildNodeView):
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,
)
db.session.add(server)
@@ -1378,7 +1387,9 @@ class ServerNode(PGChildNodeView):
server.kerberos_conn is None):
conn_passwd = getattr(conn, 'password', None)
if conn_passwd is None and not server.save_password and \
server.passfile is None and server.service is None:
server.passfile 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

View File

@@ -39,6 +39,8 @@ export default class ServerSchema extends BaseUISchema {
save_password: false,
db_res: [],
passfile: undefined,
passexec: undefined,
passexec_expiration: undefined,
sslcompression: false,
sslcert: undefined,
sslkey: undefined,
@@ -424,7 +426,21 @@ export default class ServerSchema extends BaseUISchema {
let passfile = state.passfile;
return !_.isUndefined(passfile) && !_.isNull(passfile);
},
},{
},
{
id: 'passexec_cmd', label: gettext('Password exec command'), type: 'text',
group: gettext('Advanced'),
mode: ['properties', 'edit', 'create'],
},
{
id: 'passexec_expiration', label: gettext('Password exec expiration (seconds)'), type: 'int',
group: gettext('Advanced'),
mode: ['properties', 'edit', 'create'],
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,

View File

@@ -8316,6 +8316,14 @@ msgstr ""
msgid "Password file"
msgstr ""
#: pgadmin/browser/server_groups/servers/static/js/server.ui.js:431
msgid "Password exec command"
msgstr ""
#: pgadmin/browser/server_groups/servers/static/js/server.ui.js:436
msgid "Password exec expiration (seconds)"
msgstr ""
#: pgadmin/browser/server_groups/servers/static/js/server.ui.js:438
msgid "Connection timeout (seconds)"
msgstr ""

View File

@@ -30,7 +30,7 @@ import uuid
#
##########################################################################
SCHEMA_VERSION = 33
SCHEMA_VERSION = 34
##########################################################################
#
@@ -158,6 +158,8 @@ class Server(db.Model):
)
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)
@@ -215,6 +217,8 @@ class Server(db.Model):
"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,

View File

@@ -300,6 +300,8 @@ class Connection(BaseConnection):
# 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
if manager.passexec:
password = manager.passexec.get()
try:
database = self.db

View File

@@ -13,6 +13,7 @@ Implementation of ServerManager
import os
import datetime
import config
import logging
from flask import current_app, session
from flask_security import current_user
from flask_babel import gettext
@@ -27,6 +28,7 @@ from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
CryptKeyMissing
from pgadmin.utils.master_password import get_crypt_key
from pgadmin.utils.exception import ObjectGone
from pgadmin.utils.passexec import PasswordExec
if config.SUPPORT_SSH_TUNNEL:
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
@@ -77,6 +79,9 @@ class ServerManager(object):
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
@@ -567,20 +572,28 @@ WHERE db.oid = {0}""".format(did))
try:
# If authentication method is 1 then it uses identity file
# and password
ssh_logger = None
if current_app.debug:
ssh_logger = logging.getLogger('sshtunnel')
ssh_logger.setLevel(logging.DEBUG)
for h in current_app.logger.handlers:
ssh_logger.addHandler(h)
if self.tunnel_authentication == 1:
self.tunnel_object = SSHTunnelForwarder(
(self.tunnel_host, int(self.tunnel_port)),
ssh_username=self.tunnel_username,
ssh_pkey=get_complete_file_path(self.tunnel_identity_file),
ssh_private_key_password=tunnel_password,
remote_bind_address=(self.host, self.port)
remote_bind_address=(self.host, self.port),
logger=ssh_logger
)
else:
self.tunnel_object = SSHTunnelForwarder(
(self.tunnel_host, int(self.tunnel_port)),
ssh_username=self.tunnel_username,
ssh_password=tunnel_password,
remote_bind_address=(self.host, self.port)
remote_bind_address=(self.host, self.port),
logger=ssh_logger
)
# flag tunnel threads in daemon mode to fix hang issue.
self.tunnel_object.daemon_forward_servers = True

View File

@@ -0,0 +1,65 @@
import logging
import subprocess
from datetime import datetime, timedelta
from threading import Lock
from flask import current_app
import config
class PasswordExec:
lock = Lock()
def __init__(self, cmd, expiration_seconds=None, timeout=60):
self.cmd = str(cmd)
self.expiration_seconds = int(expiration_seconds) \
if expiration_seconds is not None else None
self.timeout = int(timeout)
self.password = None
self.last_result = None
def get(self):
if config.SERVER_MODE:
# Arbitrary shell execution on server is a security risk
raise NotImplementedError('Passexec not available in server mode')
with self.lock:
if not self.password or self.is_expired():
if not self.cmd:
return None
current_app.logger.info(f'Calling passexec')
now = datetime.utcnow()
try:
p = subprocess.run(
self.cmd,
shell=True,
timeout=self.timeout,
capture_output=True,
text=True,
check=True,
)
except subprocess.CalledProcessError as e:
if (e.stderr):
self.create_logger().error(e.stderr)
raise
current_app.logger.info(f'Passexec completed successfully')
self.last_result = now
self.password = p.stdout.strip()
return self.password
def is_expired(self):
if self.expiration_seconds is None:
return False
return self.last_result is not None and\
datetime.utcnow() - self.last_result \
>= timedelta(seconds=self.expiration_seconds)
def create_logger(self):
logger = logging.getLogger('passexec')
for h in current_app.logger.handlers:
logger.addHandler(h)
return logger