diff --git a/web/config.py b/web/config.py index 5ee3410d1..5f22c881f 100644 --- a/web/config.py +++ b/web/config.py @@ -78,7 +78,10 @@ CONSOLE_LOG_FORMAT = '%(asctime)s: %(levelname)s\t%(name)s:\t%(message)s' FILE_LOG_FORMAT = '%(asctime)s: %(levelname)s\t%(name)s:\t%(message)s' # Log file name -LOG_FILE = 'pgadmin4.log' +LOG_FILE = os.path.join( + os.path.realpath(os.path.expanduser('~/.pgadmin/')), + 'pgadmin4.log' + ) ########################################################################## # Server settings @@ -144,9 +147,34 @@ SETTINGS_SCHEMA_VERSION = 8 # settings. This default places the file in the same directory as this # config file, but generates an absolute path for use througout the app. SQLITE_PATH = os.path.join( - os.path.dirname(os.path.realpath(__file__)), - 'pgadmin4.db' - ) + os.path.realpath(os.path.expanduser('~/.pgadmin/')), + 'pgadmin4.db' + ) + +########################################################################## +# Server-side session storage path +# +# SESSION_DB_PATH (Default: $HOME/.pgadmin4/sessions) +########################################################################## +# +# We use SQLite for server-side session storage. There will be one +# SQLite database object per session created. +# +# Specify the path used to store your session objects. +# +# If the specified directory does not exist, the setup script will create +# it with permission mode 700 to keep the session database secure. +# +# On certain systems, you can use shared memory (tmpfs) for maximum +# scalability, for example, on Ubuntu: +# +# SESSION_DB_PATH = '/run/shm/pgAdmin4_session' +# +########################################################################## +SESSION_DB_PATH = os.path.join( + os.path.realpath(os.path.expanduser('~/.pgadmin/')), + 'sessions' + ) ########################################################################## # Mail server settings diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 0c6f7eda4..3901c815a 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -24,6 +24,8 @@ from werkzeug.utils import find_modules import sys import logging +from pgadmin.utils.session import ServerSideSessionInterface + # Configuration settings import config @@ -99,6 +101,11 @@ def create_app(app_name=config.APP_NAME): app.jinja_env.trim_blocks = True app.config.from_object(config) + ########################################################################## + # Setup session management + ########################################################################## + app.session_interface = ServerSideSessionInterface(config.SESSION_DB_PATH) + ########################################################################## # Setup logging and log the application startup ########################################################################## diff --git a/web/pgadmin/utils/session.py b/web/pgadmin/utils/session.py new file mode 100644 index 000000000..475810255 --- /dev/null +++ b/web/pgadmin/utils/session.py @@ -0,0 +1,235 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2016, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +""" +Implements the server-side session management. + +Credit/Reference: http://flask.pocoo.org/snippets/86/ + +Modified to support both Python 2.6+ & Python 3.x +""" + +import os +import errno +import sqlite3 +from uuid import uuid4 +try: + from cPickle import dumps, loads +except: + from pickle import dumps, loads + +from collections import MutableMapping +from flask import request +from flask.sessions import SessionInterface, SessionMixin + + +class SqliteSessionStorage(MutableMapping, SessionMixin): + """ + A class to store the session as sqlite object. + """ + + _create_sql = ( + 'CREATE TABLE IF NOT EXISTS pg_session ' + '(' + ' key TEXT PRIMARY KEY,' + ' val BLOB' + ')' + ) + _get_sql = 'SELECT val FROM pg_session WHERE key = ?' + _set_sql = 'REPLACE INTO pg_session (key, val) VALUES (?, ?)' + _del_sql = 'DELETE FROM pg_session WHERE key = ?' + _ite_sql = 'SELECT key FROM pg_session' + _len_sql = 'SELECT COUNT(*) FROM pg_session' + + def __init__(self, directory, sid, *args, **kwargs): + """Initialize the session storage for this particular session. If + requires, creates new sqlite database per session (if require). + """ + self.path = os.path.join(directory, sid) + self.directory = directory + self.sid = sid + self.modified = False + self.conn = None + if not os.path.exists(self.path): + sess_db = os.open(self.path, os.O_CREAT, int("600", 8)) + os.close(sess_db) + + with self._get_conn() as conn: + conn.execute(self._create_sql) + self.new = True + + def __getitem__(self, key): + """Reads the session data for the particular key from the sqlite + database. + """ + key = dumps(key, 0) + rv = None + with self._get_conn() as conn: + for row in conn.execute(self._get_sql, (key,)): + rv = loads(str(row[0])) + break + if rv is None: + raise KeyError('Key not in this session') + return rv + + def __setitem__(self, key, value): + """Stores the session data for the given key. + """ + key = dumps(key, 0) + value = dumps(value, 2) + with self._get_conn() as conn: + conn.execute(self._set_sql, (key, sqlite3.Binary(value))) + self.modified = True + + def __delitem__(self, key): + """Removes the session data representing the key from the session. + """ + key = dumps(key, 0) + with self._get_conn() as conn: + conn.execute(self._del_sql, (key,)) + self.modified = True + + def __iter__(self): + """Returns the iterator of the key, value pair stored under this + session. + """ + with self._get_conn() as conn: + for row in conn.execute(self._ite_sql): + yield loads(str(row[0])) + + def __len__(self): + """Returns the number of keys stored in this session. + """ + with self._get_conn() as conn: + for row in conn.execute(self._len_sql): + return row[0] + + def _get_conn(self): + """Connection object to the sqlite database object. + """ + if not self.conn: + self.conn = sqlite3.Connection(self.path) + return self.conn + + # These proxy classes are needed in order + # for this session implementation to work properly. + # That is because sometimes flask will chain method calls + # with session'setdefault' calls. + # Eg: session.setdefault('_flashes', []).append(1) + # With these proxies, the changes made by chained + # method calls will be persisted back to the sqlite + # database. + class CallableAttributeProxy(object): + """ + A proxy class to represent the callable attributes of a object. + """ + + def __init__(self, session, key, obj, attr): + """Initialize the proxy instance for the callable attribute. + """ + self.session = session + self.key = key + self.obj = obj + self.attr = attr + + def __call__(self, *args, **kwargs): + """Returns the callable attributes for this session. + """ + rv = self.attr(*args, **kwargs) + self.session[self.key] = self.obj + return rv + + class PersistedObjectProxy(object): + """ + A proxy class to represent the persistent object. + """ + + def __init__(self, session, key, obj): + """Initialize the persitent objects under the session. + """ + self.session = session + self.key = key + self.obj = obj + + def __getattr__(self, name): + """Returns the attribute of the persistent object representing by + the name for this object. + """ + attr = getattr(self.obj, name) + if callable(attr): + return SqliteSessionStorage.CallableAttributeProxy( + self.session, self.key, self.obj, attr + ) + return attr + + def setdefault(self, key, value): + """Sets the default value for the particular key in the session. + """ + + if key not in self: + self[key] = value + self.modified = True + + return SqliteSessionStorage.PersistedObjectProxy( + self, key, self[key] + ) + + +class ServerSideSessionInterface(SessionInterface): + """ + Implements the SessionInterface to support saving/opening session + as sqlite object. + """ + + def __init__(self, directory): + """Initialize the session interface, which uses the sqlite as local + storage, and works as server side session manager. + + It takes directory as parameter, and creates the directory with 700 + permission (if not exists). + """ + directory = os.path.abspath(directory) + if not os.path.exists(directory): + os.makedirs(directory, int('700', 8)) + self.directory = directory + + def open_session(self, app, request): + """ + Returns the SqliteSessionStorage object representing this session. + """ + sid = request.cookies.get(app.session_cookie_name) + if not sid: + sid = str(uuid4()) + return SqliteSessionStorage(self.directory, sid) + + def save_session(self, app, session, response): + """ + Saves/Detroys the session object. + """ + sid = request.cookies.get(app.session_cookie_name) + domain = self.get_cookie_domain(app) + if not session: + try: + if session is None: + session = SqliteSessionStorage(self.directory, sid) + os.unlink(session.path) + except OSError as ex: + if ex.errno != errno.ENOENT: + raise + if session.modified: + response.delete_cookie( + app.session_cookie_name, + domain=domain + ) + return + cookie_exp = self.get_expiration_time(app, session) + response.set_cookie( + app.session_cookie_name, session.sid, + expires=cookie_exp, httponly=True, domain=domain + ) diff --git a/web/setup.py b/web/setup.py index 5b407c1e0..690744a31 100644 --- a/web/setup.py +++ b/web/setup.py @@ -287,6 +287,12 @@ Exiting...""" % (version.value)) )) do_upgrade(app, user_datastore, security, version) else: + directory = os.path.dirname(config.SQLITE_PATH) + if not os.path.exists(directory): + os.makedirs(directory, int('700', 8)) + db_file = os.open(config.SQLITE_PATH, os.O_CREAT, int('600', 8)) + os.close(db_file) + print(""" The configuration database - '{0}' does not exist. Entering initial setup mode...""".format(config.SQLITE_PATH))