Hi,

Please find the attached patch with some minor improvements.

Thanks,
Khushboo

On Wed, Apr 7, 2021 at 11:50 PM Khushboo Vashi <
khushboo.va...@enterprisedb.com> wrote:

> Hi,
>
> Please find the attached patch for RM 6158: Support Kerberos
> Authentication - Phase 2.
> This patch includes the support for logging into PostgreSQL servers with
> Kerberos authentication.
>
> Thanks,
> Khushboo
>
>
diff --git a/web/config.py b/web/config.py
index 3e19a2858..e96d60c65 100644
--- a/web/config.py
+++ b/web/config.py
@@ -634,6 +634,9 @@ KRB_KTNAME = '<KRB5_KEYTAB_FILE>'
 
 KRB_AUTO_CREATE_USER = True
 
+KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache')
+
+
 ##########################################################################
 # Local config settings
 ##########################################################################
diff --git a/web/migrations/versions/cce2006fa107_.py b/web/migrations/versions/cce2006fa107_.py
new file mode 100644
index 000000000..0cd3d7da1
--- /dev/null
+++ b/web/migrations/versions/cce2006fa107_.py
@@ -0,0 +1,28 @@
+
+"""empty message
+
+Revision ID: cce2006fa107
+Revises: a39bd015b644
+Create Date: 2021-03-15 00:02:40.100252
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from pgadmin.model import db
+
+# revision identifiers, used by Alembic.
+revision = 'cce2006fa107'
+down_revision = 'a39bd015b644'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    db.engine.execute(
+        'ALTER TABLE server ADD COLUMN kerberos_conn INTEGER DEFAULT 0'
+    )
+
+
+def downgrade():
+    # pgAdmin only upgrades, downgrade not implemented.
+    pass
diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py
index 9166c2ffd..3bc64483e 100644
--- a/web/pgadmin/authenticate/__init__.py
+++ b/web/pgadmin/authenticate/__init__.py
@@ -13,10 +13,13 @@ import flask
 import pickle
 from flask import current_app, flash, Response, request, url_for,\
     render_template
-from flask_security import current_user
+from flask_babelex import gettext
+from flask_security import current_user, login_required
 from flask_security.views import _security, _ctx
 from flask_security.utils import config_value, get_post_logout_redirect, \
     get_post_login_redirect, logout_user
+from pgadmin.utils.ajax import make_json_response, internal_server_error
+import os
 
 from flask import session
 
@@ -34,7 +37,9 @@ class AuthenticateModule(PgAdminModule):
     def get_exposed_url_endpoints(self):
         return ['authenticate.login',
                 'authenticate.kerberos_login',
-                'authenticate.kerberos_logout']
+                'authenticate.kerberos_logout',
+                'authenticate.kerberos_update_ticket',
+                'authenticate.kerberos_validate_ticket']
 
 
 blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='')
@@ -55,6 +60,12 @@ def kerberos_login():
 @pgCSRFProtect.exempt
 def kerberos_logout():
     logout_user()
+    if 'KRB5CCNAME' in session:
+        # Remove the credential cache
+        cache_file_path = session['KRB5CCNAME'].split(":")[1]
+        if os.path.exists(cache_file_path):
+            os.remove(cache_file_path)
+
     return Response(render_template("browser/kerberos_logout.html",
                                     login_url=url_for('security.login'),
                                     ))
@@ -173,11 +184,13 @@ class AuthSourceManager():
             # OR When kerberos authentication failed while accessing pgadmin,
             # we need to break the loop as no need to authenticate further
             # even if the authentication sources set to multiple
-            if not status and (hasattr(msg, 'status') and
-                               msg.status == '401 UNAUTHORIZED') or \
-                (source.get_source_name() == KERBEROS and
-                 request.method == 'GET'):
-                break
+            if not status:
+                if (hasattr(msg, 'status') and
+                    msg.status == '401 UNAUTHORIZED') or\
+                        (source.get_source_name() ==
+                         KERBEROS and
+                         request.method == 'GET'):
+                    break
 
             if status:
                 self.set_source(source)
@@ -224,3 +237,58 @@ def init_app(app):
     AuthSourceRegistry.load_auth_sources()
 
     return auth_sources
+
+
+@blueprint.route("/kerberos/update_ticket",
+                 endpoint="kerberos_update_ticket", methods=["GET"])
+@pgCSRFProtect.exempt
+@login_required
+def kerberos_update_ticket():
+    """
+    Update the kerberos ticket.
+    """
+    from werkzeug.datastructures import Headers
+    headers = Headers()
+
+    authorization = request.headers.get("Authorization", None)
+
+    if authorization is None:
+        # Send the Negotiate header to the client
+        # if Kerberos ticket is not found.
+        headers.add('WWW-Authenticate', 'Negotiate')
+        return Response("Unauthorised", 401, headers)
+    else:
+        source = get_auth_sources(KERBEROS)
+        auth_header = authorization.split()
+        in_token = auth_header[1]
+
+        # Validate the Kerberos ticket
+        status, context = source.negotiate_start(in_token)
+        if status:
+            return Response("Ticket updated successfully.")
+
+        return Response(context, 500)
+
+
+@blueprint.route("/kerberos/validate_ticket",
+                 endpoint="kerberos_validate_ticket", methods=["GET"])
+@pgCSRFProtect.exempt
+@login_required
+def kerberos_validate_ticket():
+    """
+    Return the kerberos ticket lifetime left after getting the
+    ticket from the credential cache
+    """
+    import gssapi
+
+    try:
+        del_creds = gssapi.Credentials(store={'ccache': session['KRB5CCNAME']})
+        creds = del_creds.acquire(store={'ccache': session['KRB5CCNAME']})
+    except Exception as e:
+        current_app.logger.exception(e)
+        return internal_server_error(errormsg=str(e))
+
+    return make_json_response(
+        data={'ticket_lifetime': creds.lifetime},
+        status=200
+    )
diff --git a/web/pgadmin/authenticate/kerberos.py b/web/pgadmin/authenticate/kerberos.py
index 57aa1e0f0..2f8fd0d6e 100644
--- a/web/pgadmin/authenticate/kerberos.py
+++ b/web/pgadmin/authenticate/kerberos.py
@@ -10,7 +10,7 @@
 """A blueprint module implementing the Spnego/Kerberos authentication."""
 
 import base64
-from os import environ
+from os import environ, path
 
 from werkzeug.datastructures import Headers
 from flask_babelex import gettext
@@ -128,19 +128,37 @@ class KerberosAuthentication(BaseAuthentication):
         if out_token and not context.complete:
             return False, out_token
         if context.complete:
+            deleg_creds = context.delegated_creds
+            if not hasattr(deleg_creds, 'name'):
+                error_msg = gettext('Delegated credentials not supplied.')
+                current_app.logger.error(error_msg)
+                return False, Exception(error_msg)
+            try:
+                cache_file_path = path.join(
+                    config.KERBEROS_CCACHE_DIR, 'pgadmin_cache_{0}'.format(
+                        deleg_creds.name)
+                )
+                CCACHE = 'FILE:{0}'.format(cache_file_path)
+                store = {'ccache': CCACHE}
+                deleg_creds.store(store, overwrite=True, set_default=True)
+                session['KRB5CCNAME'] = CCACHE
+            except Exception as e:
+                current_app.logger.exception(e)
+                return False, e
+
             return True, context
         else:
             return False, None
 
     def negotiate_end(self, context):
-        # Free gss_cred_id_t
+        # Free Delegated Credentials
         del_creds = getattr(context, 'delegated_creds', None)
         if del_creds:
             deleg_creds = context.delegated_creds
             del(deleg_creds)
 
     def __auto_create_user(self, username):
-        """Add the ldap user to the internal SQLite database."""
+        """Add the kerberos user to the internal SQLite database."""
         username = str(username)
         if config.KRB_AUTO_CREATE_USER:
             user = User.query.filter_by(
diff --git a/web/pgadmin/authenticate/static/js/kerberos.js b/web/pgadmin/authenticate/static/js/kerberos.js
new file mode 100644
index 000000000..dffe1d4dc
--- /dev/null
+++ b/web/pgadmin/authenticate/static/js/kerberos.js
@@ -0,0 +1,55 @@
+import url_for from 'sources/url_for';
+
+function fetch_ticket() {
+  // Fetch the Kerberos Updated ticket through SPNEGO
+  return fetch(url_for('authenticate.kerberos_update_ticket')
+  )
+    .then(function(response){
+      if (response.status >= 200 && response.status < 300) {
+        return Promise.resolve(response);
+      } else {
+        return Promise.reject(new Error(response.statusText));
+      }
+    });
+}
+
+function fetch_ticket_lifetime () {
+  // Fetch the Kerberos ticket lifetime left
+
+  return fetch(url_for('authenticate.kerberos_validate_ticket')
+  )
+    .then(
+      function(response){
+        if (response.status >= 200 && response.status < 300) {
+          return response.json();
+        } else {
+          return Promise.reject(new Error(response.statusText));
+        }
+      }
+    )
+    .then(function(response){
+      let ticket_lifetime = response.data.ticket_lifetime;
+      if (ticket_lifetime > 0) {
+        return Promise.resolve(ticket_lifetime);
+      } else {
+        return Promise.reject();
+      }
+    });
+
+}
+
+function validate_kerberos_ticket() {
+  // Ping pgAdmin server every 10 seconds
+  // to fetch the Kerberos ticket lifetime left
+  return setInterval(function() {
+    let newPromise = fetch_ticket_lifetime();
+    newPromise.then(
+      function() {
+        return;
+      },
+      fetch_ticket
+    );
+  }, 10000);
+}
+
+export {fetch_ticket, validate_kerberos_ticket, fetch_ticket_lifetime};
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index bc0bd4611..a7e5288e5 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -253,7 +253,8 @@ class ServerModule(sg.ServerGroupPluginModule):
                 errmsg=errmsg,
                 user_id=server.user_id,
                 user_name=server.username,
-                shared=server.shared
+                shared=server.shared,
+                is_kerberos_conn=bool(server.kerberos_conn),
             )
 
     @property
@@ -546,7 +547,8 @@ class ServerNode(PGChildNodeView):
                     if server.tunnel_password is not None else False,
                     errmsg=errmsg,
                     user_name=server.username,
-                    shared=server.shared
+                    shared=server.shared,
+                    is_kerberos_conn=bool(server.kerberos_conn)
                 )
             )
 
@@ -613,7 +615,8 @@ class ServerNode(PGChildNodeView):
                 if server.tunnel_password is not None else False,
                 errmsg=errmsg,
                 shared=server.shared,
-                user_name=server.username
+                user_name=server.username,
+                is_kerberos_conn=bool(server.kerberos_conn)
             ),
         )
 
@@ -719,7 +722,8 @@ class ServerNode(PGChildNodeView):
             'tunnel_username': 'tunnel_username',
             'tunnel_authentication': 'tunnel_authentication',
             'tunnel_identity_file': 'tunnel_identity_file',
-            'shared': 'shared'
+            'shared': 'shared',
+            'kerberos_conn': 'kerberos_conn',
         }
 
         disp_lbl = {
@@ -983,7 +987,8 @@ class ServerNode(PGChildNodeView):
             'tunnel_username': tunnel_username,
             'tunnel_identity_file': server.tunnel_identity_file
             if server.tunnel_identity_file else None,
-            'tunnel_authentication': tunnel_authentication
+            'tunnel_authentication': tunnel_authentication,
+            'kerberos_conn': bool(server.kerberos_conn),
         }
 
         return ajax_response(response)
@@ -1070,7 +1075,8 @@ class ServerNode(PGChildNodeView):
                 tunnel_authentication=data.get('tunnel_authentication', 0),
                 tunnel_identity_file=data.get('tunnel_identity_file', None),
                 shared=data.get('shared', None),
-                passfile=data.get('passfile', None)
+                passfile=data.get('passfile', None),
+                kerberos_conn=1 if data.get('kerberos_conn', False) else 0,
             )
             db.session.add(server)
             db.session.commit()
@@ -1152,7 +1158,8 @@ class ServerNode(PGChildNodeView):
                     else 'pg',
                     version=manager.version
                     if manager and manager.version
-                    else None
+                    else None,
+                    is_kerberos_conn=bool(server.kerberos_conn),
                 )
             )
 
@@ -1346,7 +1353,7 @@ class ServerNode(PGChildNodeView):
                 except Exception as e:
                     current_app.logger.exception(e)
                     return internal_server_error(errormsg=str(e))
-        if 'password' not in data:
+        if 'password' not in data and server.kerberos_conn is False:
             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:
@@ -1355,7 +1362,7 @@ class ServerNode(PGChildNodeView):
                 passfile = server.passfile
             else:
                 password = conn_passwd or server.password
-        else:
+        elif server.kerberos_conn is False:
             password = data['password'] if 'password' in data else None
             save_password = data['save_password']\
                 if 'save_password' in data else False
@@ -1398,6 +1405,9 @@ class ServerNode(PGChildNodeView):
                 "Could not connect to server(#{0}) - '{1}'.\nError: {2}"
                 .format(server.id, server.name, errmsg)
             )
+            if errmsg.find('Ticket expired') != -1:
+                return internal_server_error(errmsg)
+
             return self.get_response_for_password(server, 401, True,
                                                   True, errmsg)
         else:
@@ -1465,6 +1475,7 @@ class ServerNode(PGChildNodeView):
                     'is_password_saved': bool(server.save_password),
                     'is_tunnel_password_saved': True
                     if server.tunnel_password is not None else False,
+                    'is_kerberos_conn': bool(server.kerberos_conn),
                 }
             )
 
diff --git a/web/pgadmin/browser/server_groups/servers/databases/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/__init__.py
index 60af1de42..4b1d7308d 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/__init__.py
@@ -490,6 +490,7 @@ class DatabaseView(PGChildNodeView):
                         did, errmsg
                     )
                 )
+
                 return internal_server_error(errmsg)
             else:
                 current_app.logger.info(
diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
index c53f04429..01ab89c50 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
@@ -10,9 +10,10 @@
 define('pgadmin.node.database', [
   'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
   'sources/utils', 'sources/pgadmin', 'pgadmin.browser.utils',
-  'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.browser.collection',
+  'pgadmin.alertifyjs', 'pgadmin.backform',
+  'pgadmin.authenticate.kerberos', 'pgadmin.browser.collection',
   'pgadmin.browser.server.privilege', 'pgadmin.browser.server.variable',
-], function(gettext, url_for, $, _, pgadminUtils, pgAdmin, pgBrowser, Alertify, Backform) {
+], function(gettext, url_for, $, _, pgadminUtils, pgAdmin, pgBrowser, Alertify, Backform, Kerberos) {
 
   if (!pgBrowser.Nodes['coll-database']) {
     pgBrowser.Nodes['coll-database'] =
@@ -556,24 +557,39 @@ define('pgadmin.node.database', [
           onFailure = function(
             xhr, status, error, _model, _data, _tree, _item, _status
           ) {
-            if (!_status) {
-              tree.setInode(_item);
-              tree.addIcon(_item, {icon: 'icon-database-not-connected'});
-            }
-
-            Alertify.pgNotifier('error', xhr, error, function(msg) {
-              setTimeout(function() {
-                if (msg == 'CRYPTKEY_SET') {
+            if (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1) {
+              tree.addIcon(_item, {icon: 'icon-server-connecting'});
+              let fetchTicket = Kerberos.fetch_ticket();
+              fetchTicket.then(
+                function() {
                   connect_to_database(_model, _data, _tree, _item, _wasConnected);
-                } else {
-                  Alertify.dlgServerPass(
-                    gettext('Connect to database'),
-                    msg, _model, _data, _tree, _item, _status,
-                    onSuccess, onFailure, onCancel
-                  ).resizeTo();
+                },
+                function(error) {
+                  tree.setInode(_item);
+                  tree.addIcon(_item, {icon: 'icon-database-not-connected'});
+                  Alertify.pgNotifier(error, xhr, gettext('Connect  to database.'));
                 }
-              }, 100);
-            });
+              );
+            } else {
+              if (!_status) {
+                tree.setInode(_item);
+                tree.addIcon(_item, {icon: 'icon-database-not-connected'});
+              }
+
+              Alertify.pgNotifier('error', xhr, error, function(msg) {
+                setTimeout(function() {
+                  if (msg == 'CRYPTKEY_SET') {
+                    connect_to_database(_model, _data, _tree, _item, _wasConnected);
+                  } else {
+                    Alertify.dlgServerPass(
+                      gettext('Connect to database'),
+                      msg, _model, _data, _tree, _item, _status,
+                      onSuccess, onFailure, onCancel
+                    ).resizeTo();
+                  }
+                }, 100);
+              });
+            }
           },
           onSuccess = function(
             res, model, _data, _tree, _item, _connected
@@ -640,6 +656,7 @@ define('pgadmin.node.database', [
           if (xhr.status === 410) {
             error = gettext('Error: Object not found - %s.', error);
           }
+
           return onFailure(
             xhr, status, error, obj, data, tree, item, wasConnected
           );
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 ab95d6d89..deee434bd 100644
--- a/web/pgadmin/browser/server_groups/servers/static/js/server.js
+++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js
@@ -13,11 +13,12 @@ define('pgadmin.node.server', [
   'pgadmin.server.supported_servers', 'pgadmin.user_management.current_user',
   'pgadmin.alertifyjs', 'pgadmin.backform',
   'sources/browser/server_groups/servers/model_validation',
+  'pgadmin.authenticate.kerberos',
   'pgadmin.browser.server.privilege',
 ], function(
   gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser,
   supported_servers, current_user, Alertify, Backform,
-  modelValidation
+  modelValidation, Kerberos
 ) {
 
   if (!pgBrowser.Nodes['server']) {
@@ -904,20 +905,32 @@ define('pgadmin.node.server', [
               }
             },
           }),
+        },{
+          id: 'kerberos_conn', label: gettext('Kerberos authentication?'), type: 'switch',
+          group: gettext('Connection'), 'options': {
+            'onText':  gettext('True'), 'offText':  gettext('False'), 'size': 'mini',
+          },
         },{
           id: 'password', label: gettext('Password'), type: 'password', maxlength: null,
-          group: gettext('Connection'), control: 'input', mode: ['create'], deps: ['connect_now'],
+          group: gettext('Connection'), control: 'input', mode: ['create'],
+          deps: ['connect_now', 'kerberos_conn'],
           visible: function(model) {
             return model.get('connect_now') && model.isNew();
           },
+          disabled: function(model) {
+            if (model.get('kerberos_conn'))
+              return true;
+
+            return false;
+          },
         },{
           id: 'save_password', controlLabel: gettext('Save password?'),
           type: 'checkbox', group: gettext('Connection'), mode: ['create'],
-          deps: ['connect_now'], visible: function(model) {
+          deps: ['connect_now', 'kerberos_conn'], visible: function(model) {
             return model.get('connect_now') && model.isNew();
           },
-          disabled: function() {
-            if (!current_user.allow_save_password)
+          disabled: function(model) {
+            if (!current_user.allow_save_password || model.get('kerberos_conn'))
               return true;
 
             return false;
@@ -1279,19 +1292,32 @@ define('pgadmin.node.server', [
             }
 
           }
-
-          Alertify.pgNotifier('error', xhr, error, function(msg) {
-            setTimeout(function() {
-              if (msg == 'CRYPTKEY_SET') {
+          if (_data.is_kerberos_conn === true || (xhr.status != 200 && xhr.responseText.search('Ticket expired') !== -1)) {
+            tree.addIcon(_item, {icon: 'icon-server-connecting'});
+            let fetchTicket = Kerberos.fetch_ticket();
+            fetchTicket.then(
+              function() {
                 connect_to_server(_node, _data, _tree, _item, _wasConnected);
-              } else {
-                Alertify.dlgServerPass(
-                  gettext('Connect to Server'),
-                  msg, _node, _data, _tree, _item, _wasConnected
-                ).resizeTo();
+              },
+              function() {
+                tree.addIcon(_item, {icon: 'icon-server-not-connected'});
+                Alertify.pgNotifier('Connection error', xhr, gettext('Connect to server.'));
               }
-            }, 100);
-          });
+            );
+          } else {
+            Alertify.pgNotifier('error', xhr, error, function(msg) {
+              setTimeout(function() {
+                if (msg == 'CRYPTKEY_SET') {
+                  connect_to_server(_node, _data, _tree, _item, _wasConnected);
+                } else {
+                  Alertify.dlgServerPass(
+                    gettext('Connect to Server'),
+                    msg, _node, _data, _tree, _item, _wasConnected
+                  ).resizeTo();
+                }
+              }, 100);
+            });
+          }
         },
         onSuccess = function(res, node, _data, _tree, _item, _wasConnected) {
           if (res && res.data) {
diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index 4ffb5ee5a..bf44aa6f4 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -12,19 +12,22 @@ define('pgadmin.browser', [
   'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore',
   'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror',
   'sources/check_node_visibility', './toolbar', 'pgadmin.help',
-  'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.browser.utils',
-  'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree',
+  'sources/csrf', 'sources/utils', 'sources/window', 'pgadmin.authenticate.kerberos',
+  'pgadmin.browser.utils', 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin',
+  'jquery.acitree',
   'pgadmin.browser.preferences', 'pgadmin.browser.messages',
   'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout',
   'pgadmin.browser.runtime', 'pgadmin.browser.error', 'pgadmin.browser.frame',
   'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity',
   'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
-  'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', 'jquery.acifragment',
+  'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable',
+  'jquery.acifragment',
 ], function(
   tree,
   gettext, url_for, require, $, _,
   Bootstrap, pgAdmin, Alertify, codemirror,
-  checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow
+  checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, pgWindow,
+  Kerberos
 ) {
   window.jQuery = window.$ = $;
   // Some scripts do export their object in the window only.
@@ -38,6 +41,8 @@ define('pgadmin.browser', [
 
   csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
 
+  Kerberos.validate_kerberos_ticket();
+
   var panelEvents = {};
   panelEvents[wcDocker.EVENT.VISIBILITY_CHANGED] = function() {
     if (this.isVisible()) {
diff --git a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py
index f31e983ff..6b61dc1d0 100644
--- a/web/pgadmin/browser/tests/test_kerberos_with_mocking.py
+++ b/web/pgadmin/browser/tests/test_kerberos_with_mocking.py
@@ -12,6 +12,7 @@ from pgadmin.utils.route import BaseTestGenerator
 from regression.python_test_utils import test_utils as utils
 from pgadmin.authenticate.registry import AuthSourceRegistry
 from unittest.mock import patch, MagicMock
+from werkzeug.datastructures import Headers
 
 
 class KerberosLoginMockTestCase(BaseTestGenerator):
@@ -30,6 +31,11 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
             auth_source=['kerberos'],
             auto_create_user=True,
             flag=2
+        )),
+        ('Spnego/Kerberos Update Ticket', dict(
+            auth_source=['kerberos'],
+            auto_create_user=True,
+            flag=3
         ))
     ]
 
@@ -54,8 +60,13 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
                 self.skipTest(
                     "Can not run Kerberos Authentication in the Desktop mode."
                 )
-
             self.test_authorized()
+        elif self.flag == 3:
+            if app_config.SERVER_MODE is False:
+                self.skipTest(
+                    "Can not run Kerberos Authentication in the Desktop mode."
+                )
+            self.test_update_ticket()
 
     def test_unauthorized(self):
         """
@@ -73,13 +84,7 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
         passed on to the routed method.
         """
 
-        class delCrads:
-            def __init__(self):
-                self.initiator_name = 'u...@pgadmin.org'
-        del_crads = delCrads()
-
-        AuthSourceRegistry.registry['kerberos'].negotiate_start = MagicMock(
-            return_value=[True, del_crads])
+        del_crads = self.mock_negotiate_start()
         res = self.tester.login(None,
                                 None,
                                 True,
@@ -89,6 +94,33 @@ class KerberosLoginMockTestCase(BaseTestGenerator):
         respdata = 'Gravatar image for %s' % del_crads.initiator_name
         self.assertTrue(respdata in res.data.decode('utf8'))
 
+    def mock_negotiate_start(self):
+        class delCrads:
+            def __init__(self):
+                self.initiator_name = 'u...@pgadmin.org'
+
+        del_crads = delCrads()
+
+        AuthSourceRegistry.registry['kerberos'].negotiate_start = MagicMock(
+            return_value=[True, del_crads])
+        return del_crads
+
+    def test_update_ticket(self):
+        # Response header should include the Negotiate header in the first call
+        response = self.tester.get('/authenticate/kerberos/update_ticket')
+        self.assertEqual(response.status_code, 401)
+        self.assertEqual(response.headers.get('www-authenticate'), 'Negotiate')
+
+        # When we send the Kerberos Ticket, it should return  success
+        del_crads = self.mock_negotiate_start()
+
+        krb_token = Headers({})
+        krb_token['Authorization'] = 'Negotiate CTOKEN'
+
+        response = self.tester.get('/authenticate/kerberos/update_ticket',
+                                   headers=krb_token)
+        self.assertEqual(response.status_code, 200)
+
     def tearDown(self):
         self.app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
 
diff --git a/web/pgadmin/misc/bgprocess/processes.py b/web/pgadmin/misc/bgprocess/processes.py
index ef6cfc3f2..25e0a2a9e 100644
--- a/web/pgadmin/misc/bgprocess/processes.py
+++ b/web/pgadmin/misc/bgprocess/processes.py
@@ -24,10 +24,11 @@ import logging
 from pgadmin.utils import u_encode, file_quote, fs_encoding, \
     get_complete_file_path, get_storage_directory, IS_WIN
 from pgadmin.browser.server_groups.servers.utils import does_server_exists
+from pgadmin.utils.constants import KERBEROS
 
 import pytz
 from dateutil import parser
-from flask import current_app
+from flask import current_app, session
 from flask_babelex import gettext as _
 from flask_security import current_user
 
@@ -278,13 +279,16 @@ class BatchProcess(object):
         env['PROCID'] = self.id
         env['OUTDIR'] = self.log_dir
         env['PGA_BGP_FOREGROUND'] = "1"
+        if config.SERVER_MODE and session and \
+                session['_auth_source_manager_obj']['current_source'] == \
+                KERBEROS:
+            env['KRB5CCNAME'] = session['KRB5CCNAME']
 
         if self.env:
             env.update(self.env)
 
         if cb is not None:
             cb(env)
-
         if os.name == 'nt':
             DETACHED_PROCESS = 0x00000008
             from subprocess import CREATE_NEW_PROCESS_GROUP
diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py
index d1f498181..dfacf6322 100644
--- a/web/pgadmin/model/__init__.py
+++ b/web/pgadmin/model/__init__.py
@@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy
 #
 ##########################################################################
 
-SCHEMA_VERSION = 27
+SCHEMA_VERSION = 28
 
 ##########################################################################
 #
@@ -184,6 +184,7 @@ class Server(db.Model):
     tunnel_identity_file = db.Column(db.String(64), nullable=True)
     tunnel_password = db.Column(db.String(64), nullable=True)
     shared = db.Column(db.Boolean(), nullable=False)
+    kerberos_conn = db.Column(db.Boolean(), nullable=False)
 
     @property
     def serialize(self):
diff --git a/web/pgadmin/setup/data_directory.py b/web/pgadmin/setup/data_directory.py
index 2335b0790..7a3654b77 100644
--- a/web/pgadmin/setup/data_directory.py
+++ b/web/pgadmin/setup/data_directory.py
@@ -104,3 +104,19 @@ def create_app_data_directory(config):
                 getpass.getuser(),
                 config.APP_VERSION))
         exit(1)
+
+    # Create Kerberos Credential Cache directory (if not present).
+    try:
+        _create_directory_if_not_exists(config.KERBEROS_CCACHE_DIR)
+    except PermissionError as e:
+        print(FAILED_CREATE_DIR.format(config.KERBEROS_CCACHE_DIR, e))
+        print(
+            "HINT   : Create the directory {}, ensure it is writable by\n"
+            "         '{}', and try again, or, create a config_local.py file\n"
+            "         and override the KERBEROS_CCACHE_DIR setting per\n"
+            "         https://www.pgadmin.org/docs/pgadmin4/{}/config_py.html";.
+            format(
+                config.KERBEROS_CCACHE_DIR,
+                getpass.getuser(),
+                config.APP_VERSION))
+        exit(1)
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
index 4f89e5bb7..5e4db20a9 100644
--- a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
@@ -13,6 +13,7 @@ import gettext from '../../../../static/js/gettext';
 import url_for from '../../../../static/js/url_for';
 import _ from 'underscore';
 import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+import {fetch_ticket_lifetime} from  '../../../../authenticate/static/js/kerberos';
 
 export class BackupDialogWrapper extends DialogWrapper {
   constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
@@ -165,10 +166,29 @@ export class BackupDialogWrapper extends DialogWrapper {
       );
 
       this.setExtraParameters(selectedTreeNode, treeInfo);
+      let backupDate = this.view.model.toJSON();
+
+      if(backupDate.type == 'globals' || backupDate.type == 'server') {
+        let newPromise = fetch_ticket_lifetime();
+        newPromise.then(
+          function(lifetime) {
+            if (lifetime < 1800 && lifetime  > 0) {
+              dialog.alertify.warning(
+                'You have '+ (Math.round(parseInt(lifetime)/60)).toString() +' minutes left on your ticket - if the dump takes longer than that, it may fail."'
+              );
+            }
+          },
+          function() {
+            dialog.alertify.warning(
+              gettext('Please renew your kerberos ticket, it has been expired.')
+            );
+          }
+        );
+      }
 
       axios.post(
         baseUrl,
-        this.view.model.toJSON()
+        backupDate
       ).then(function (res) {
         if (res.data.success) {
           dialog.alertify.success(gettext('Backup job created.'), 5);
diff --git a/web/pgadmin/tools/debugger/static/js/debugger.js b/web/pgadmin/tools/debugger/static/js/debugger.js
index f31a0fc00..460a200bb 100644
--- a/web/pgadmin/tools/debugger/static/js/debugger.js
+++ b/web/pgadmin/tools/debugger/static/js/debugger.js
@@ -13,11 +13,11 @@ define([
   'backbone', 'pgadmin.backgrid', 'codemirror', 'pgadmin.backform',
   'pgadmin.tools.debugger.ui', 'pgadmin.tools.debugger.utils',
   'tools/datagrid/static/js/show_query_tool', 'sources/utils',
-  'wcdocker', 'pgadmin.browser.frame',
+  'pgadmin.authenticate.kerberos', 'wcdocker', 'pgadmin.browser.frame',
 ], function(
   gettext, url_for, $, _, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
   CodeMirror, Backform, get_function_arguments, debuggerUtils, showQueryTool,
-  pgadminUtils,
+  pgadminUtils, Kerberos
 ) {
   var pgTools = pgAdmin.Tools = pgAdmin.Tools || {},
     wcDocker = window.wcDocker;
@@ -472,8 +472,20 @@ define([
         .fail(function(xhr) {
           try {
             var err = JSON.parse(xhr.responseText);
-            if (err.success == 0) {
-              Alertify.alert(gettext('Debugger Error'), err.errormsg);
+            if (err.errormsg.search('Ticket expired') !== -1) {
+              let fetchTicket = Kerberos.fetch_ticket();
+              fetchTicket.then(
+                function() {
+                  self.start_global_debugger();
+                },
+                function(error) {
+                  Alertify.alert(gettext('Debugger Error'), error);
+                }
+              );
+            } else {
+              if (err.success == 0) {
+                Alertify.alert(gettext('Debugger Error'), err.errormsg);
+              }
             }
           } catch (e) {
             console.warn(e.stack || e);
diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
index b5503255c..631e9d0da 100644
--- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
@@ -51,6 +51,7 @@ define('tools.querytool', [
   'sources/window',
   'sources/is_native',
   'sources/sqleditor/macro',
+  'pgadmin.authenticate.kerberos',
   'sources/../bundle/slickgrid',
   'pgadmin.file_manager',
   'slick.pgadmin.formatters',
@@ -65,7 +66,7 @@ define('tools.querytool', [
   GeometryViewer, historyColl, queryHist, querySources,
   keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
   modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc,
-  pgWindow, isNative, MacroHandler) {
+  pgWindow, isNative, MacroHandler, Kerberos) {
   /* Return back, this has been called more than once */
   if (pgAdmin.SqlEditor)
     return pgAdmin.SqlEditor;
@@ -2441,9 +2442,23 @@ define('tools.querytool', [
               pgBrowser.report_error(gettext('Error fetching rows - %s.', xhr.statusText), xhr.responseJSON.errormsg, undefined, self.close.bind(self));
             }
           } else {
-            pgBrowser.Events.trigger(
-              'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error
-            );
+            if (xhr.responseText.search('Ticket expired') !== -1) {
+              let fetchTicket = Kerberos.fetch_ticket();
+              fetchTicket.then(
+                function() {
+                  self.initTransaction();
+                },
+                function(error) {
+                  pgBrowser.Events.trigger(
+                    'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error
+                  );
+                }
+              );
+            } else {
+              pgBrowser.Events.trigger(
+                'pgadmin:query_tool:connected_fail:' + self.transId, xhr, error
+              );
+            }
           }
         });
       },
diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py
index de9547322..43f6e6de0 100644
--- a/web/pgadmin/utils/driver/psycopg2/connection.py
+++ b/web/pgadmin/utils/driver/psycopg2/connection.py
@@ -18,11 +18,13 @@ import select
 import datetime
 from collections import deque
 import psycopg2
-from flask import g, current_app
+import threading
+from flask import g, current_app, session
 from flask_babelex import gettext
 from flask_security import current_user
 from pgadmin.utils.crypto import decrypt, encrypt
 from psycopg2.extensions import encodings
+from os import environ
 
 import config
 from pgadmin.model import User
@@ -38,6 +40,9 @@ from .encoding import get_encoding, configure_driver_encodings
 from pgadmin.utils import csv
 from pgadmin.utils.master_password import get_crypt_key
 from io import StringIO
+from pgadmin.utils.constants import KERBEROS
+
+lock = threading.Lock()
 
 _ = gettext
 
@@ -313,6 +318,12 @@ class Connection(BaseConnection):
             os.environ['PGAPPNAME'] = '{0} - {1}'.format(
                 config.APP_NAME, conn_id)
 
+            if config.SERVER_MODE and \
+                    session['_auth_source_manager_obj']['current_source'] == \
+                    KERBEROS and 'KRB5CCNAME' in session:
+                lock.acquire()
+                environ['KRB5CCNAME'] = session['KRB5CCNAME']
+
             pg_conn = psycopg2.connect(
                 host=manager.local_bind_host if manager.use_ssh_tunnel
                 else manager.host,
@@ -340,7 +351,13 @@ class Connection(BaseConnection):
             if self.async_ == 1:
                 self._wait(pg_conn)
 
+            if config.SERVER_MODE and \
+                    session['_auth_source_manager_obj']['current_source'] == \
+                    KERBEROS:
+                environ['KRB5CCNAME'] = ''
+
         except psycopg2.Error as e:
+            environ['KRB5CCNAME'] = ''
             manager.stop_ssh_tunnel()
             if e.pgerror:
                 msg = e.pgerror
@@ -358,6 +375,11 @@ class Connection(BaseConnection):
                 )
             )
             return False, msg
+        finally:
+            if config.SERVER_MODE and \
+                    session['_auth_source_manager_obj']['current_source'] == \
+                    KERBEROS and lock.locked():
+                lock.release()
 
         # Overwrite connection notice attr to support
         # more than 50 notices at a time
@@ -1435,7 +1457,6 @@ Failed to reset the connection to the server due to following error:
         Args:
             conn: connection object
         """
-
         while True:
             state = conn.poll()
             if state == psycopg2.extensions.POLL_OK:
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 96d5b27f6..00daa12ec 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -174,6 +174,7 @@ var webpackShimConfig = {
     'pgadmin.backgrid': path.join(__dirname, './pgadmin/static/js/backgrid.pgadmin'),
 
     'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'),
+    'pgadmin.authenticate.kerberos': path.join(__dirname, './pgadmin/authenticate/static/js/kerberos'),
     'pgadmin.browser': path.join(__dirname, './pgadmin/browser/static/js/browser'),
     'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'),
     'pgadmin.browser.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'),

Reply via email to