mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-21 16:27:39 -06:00
Added support for IAM token based authentication for AWS RDS or Azure DB. #3491
This commit is contained in:
parent
25be215180
commit
a62fc2fbff
Binary file not shown.
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 68 KiB |
@ -189,6 +189,16 @@ Use the fields in the *Advanced* tab to configure a connection:
|
||||
(.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 <https://www.postgresql.org/docs/current/libpq-pgpass.html>`_.
|
||||
* 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
|
||||
should be generated as a transient authorization token instead of providing a
|
||||
password when connecting in `PAM authentication <https://www.postgresql.org/docs/current/auth-pam.html>`_ scenarios.
|
||||
* Use the *Password exec expiration* field to specify a maximum age, in seconds,
|
||||
of the password generated with a *Password exec command*. If not specified,
|
||||
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
|
||||
@ -197,6 +207,8 @@ Use the fields in the *Advanced* tab to configure a connection:
|
||||
.. note:: The password file option is only supported when pgAdmin is using libpq
|
||||
v10.0 or later to connect to the server.
|
||||
|
||||
.. note:: The Password exec option is only supported when pgAdmin is run in desktop mode.
|
||||
|
||||
* Click the *Save* button to save your work.
|
||||
* Click the *Close* button to exit without saving your work.
|
||||
* Click the *Reset* button to return the values specified on the Server dialog
|
||||
|
40
web/migrations/versions/f79844e926ae_.py
Normal file
40
web/migrations/versions/f79844e926ae_.py
Normal file
@ -0,0 +1,40 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""Update DB to version 32
|
||||
|
||||
Added passexec_cmd and passexec_expiration columns to server configuration.
|
||||
|
||||
Revision ID: f79844e926ae
|
||||
Revises: 1586db67b98e
|
||||
Create Date: 2022-10-11 11:25:00.000000
|
||||
|
||||
"""
|
||||
from pgadmin.model import db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f79844e926ae'
|
||||
down_revision = '1586db67b98e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
db.engine.execute("""
|
||||
ALTER TABLE server ADD COLUMN passexec_cmd TEXT(256) null
|
||||
""")
|
||||
db.engine.execute("""
|
||||
ALTER TABLE server ADD COLUMN passexec_expiration INT null
|
||||
""")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# pgAdmin only upgrades, downgrade not implemented.
|
||||
pass
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 ""
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
65
web/pgadmin/utils/passexec.py
Normal file
65
web/pgadmin/utils/passexec.py
Normal 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
|
Loading…
Reference in New Issue
Block a user