Source code for iocbio.kinetics.io.db

from PyQt5.QtWidgets import QDialog, QLineEdit, QPushButton, QGridLayout, QMessageBox, QLabel, QCheckBox, QFileDialog
from PyQt5.QtCore import QSettings
import keyring, sys, getpass
from records import Database
from sys import version_info
import os


def mkdir_p(path):
    import errno
    try:
        os.makedirs(path)
    except OSError as exc:  # Python >2.5
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            raise

class Login(QDialog):
    def __init__(self):
        QDialog.__init__(self)
        self.setWindowTitle("PostgreSQL login")
        self.textName = QLineEdit(self)
        self.textPass = QLineEdit(self)
        self.textPass.setEchoMode(QLineEdit.Password)
        self.buttonLogin = QPushButton('Login', self)
        self.buttonLogin.clicked.connect(self.handleLogin)
        self.rememberPassword = QCheckBox("Store password in keyring")
        self.rememberPassword.setCheckState(False)
        layout = QGridLayout(self)
        layout.addWidget(QLabel("User:"), 0, 0)
        layout.addWidget(self.textName, 0, 1)
        layout.addWidget(QLabel("Password:"), 1, 0)
        layout.addWidget(self.textPass, 1, 1)
        layout.addWidget(self.buttonLogin,2,1)
        layout.addWidget(self.rememberPassword,3,1)

    def handleLogin(self):
        self.username = self.textName.text()
        self.password = self.textPass.text()
        self.accept()
        # if (self.textName.text() == 'foo' and
        #     self.textPass.text() == 'bar'):
        #     self.accept()
        # else:
        #     QMessageBox.warning(self, 'Error', 'You failed and this incident will be forgotten')

[docs]class DatabaseInterface: """Database interface and helper functions""" schema_version = "1" def __init__(self, dbtype="sqlite3"): self.dbtype = dbtype self.database = None self.read_only = False self.disable_read_only = False if dbtype == "sqlite3": self.open_sqlite3() elif dbtype == "postgresql": self.open_postgresql() else: raise NotImplementedError("Not implemented: " + dbtype) def set_read_only(self, state): if self.disable_read_only: self.read_only = False else: self.read_only = state def open_sqlite3(self): import sqlite3, os from PyQt5.QtCore import QStandardPaths path = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) mkdir_p(path) #self.database = sqlite3.connect(os.path.join(path,"kinetics.sqlite")) self.database = Database("sqlite:///" + os.path.join(path,"kinetics.sqlite")) def open_postgresql(self): settings = QSettings() save = False#, False username = str(settings.value("postgresql/username", defaultValue = "")) try: password = keyring.get_password("iocbio-kinetics", username) except: print('Failed to get password from keyring. Looking password from crypted folder') pwd_file = settings.value("postgresql/pwd_file", defaultValue=None) if pwd_file is None: password = None else: if os.path.isfile(pwd_file): with open(pwd_file, 'r') as f: password = f.read() else: password = None if len(username) < 1 or password is None: login = Login() if login.exec_() == QDialog.Accepted: username = login.username password = login.password save = login.rememberPassword.checkState() else: sys.exit(0) try: self.database = Database("postgresql://" + username + ":" + password + "@sysbio-db.kybi/experiments_v2") except Exception as expt: if not save: # probably wrong uid/pwd, cleanup settings.setValue("postgresql/username", "") try: keyring.delete_password("iocbio-kinetics", username) except: print('Faild to delete password from keyring') print('Exception', expt) QMessageBox.warning(None, 'Error', "Failed to open PostgreSQL database connection\n\nException: " + str(expt)) sys.exit(0) if save: # let's save the settings settings.setValue("postgresql/username", username) try: keyring.set_password("iocbio-kinetics", username, password) except: path = QFileDialog.getExistingDirectory(None, 'Set secure location for saving password', '', QFileDialog.ShowDirsOnly) if path != '': pwd_file = os.path.join(path, username) settings.setValue("postgresql/pwd_file", pwd_file) with open(pwd_file, 'w') as f: f.write(password) else: print('You clicked cancel, password not saved') def close(self): if self.database is not None: self.database.close() self.database = None def query(self, command, **kwargs): if self.read_only: for k in ['insert ', 'create ', 'update ', 'set ', 'delete ']: if k in command.lower(): print('Read only mode, no data changes allowed. Skipped:', command) return None return self.database.query(command, **kwargs)
[docs] def schema(self): """Check the present schema version, create if missing and return the version of current schema""" c = self.database c.query("CREATE TABLE IF NOT EXISTS " + self.table("iocbio") + "(name text NOT NULL PRIMARY KEY, value text NOT NULL)") version = None for row in c.query("SELECT value FROM %s WHERE name=:name" % self.table("iocbio"), name="version"): version = row.value if version is None: c.query("INSERT INTO %s(name, value) VALUES(:name,:value)" % self.table("iocbio"), name="version", value=self.schema_version) version = self.schema_version return version
def has_record(self, table, **kwargs): c = self.database a = [] sql = "SELECT 1 FROM " + self.table(table) + " WHERE " for key in kwargs.keys(): sql += key + "=:" + key + " AND " sql = sql[:-5] # dropping excessive " AND " sql += " LIMIT 1" for row in c.query(sql, **kwargs): return True return False def table(self, name): # IF CHANGED HERE, CHECK OUT also the following methods # has_view, grant if self.dbtype == "sqlite3": return name elif self.dbtype == "postgresql": return "kinetics_" + name else: raise NotImplementedError("Not implemented table name mangling: " + self.dbtype) def has_view(self, view): if self.dbtype == "sqlite3": for row in self.database.query("SELECT 1 AS reply FROM sqlite_master WHERE type='view' AND " + "name=:view", view=self.table(view)): return True elif self.dbtype == "postgresql": for row in self.database.query("SELECT 1 AS reply FROM information_schema.views WHERE " + "table_schema=:schema AND table_name=lower(:view)", schema="public", view=self.table(view)): return True else: raise NotImplementedError("Not implemented table name mangling: " + self.dbtype) return False @staticmethod def _managed_table(name, strict=False, candelete=False): for i in ['pg_', 'sql_', '_pg', 'information_']: if name.find(i) == 0: return False if candelete: if 'kinetics_ioc' in name or "kinetics_experiment" in name: return False if strict: return 'kinetics_' in name return True
[docs] def grant_groups(self): """Set default permissions for all tables""" if self.dbtype not in ["postgresql"]: print("Permissions model not supported for", self.dbtype) if self.dbtype == "postgresql": # allow select to PUBLIC self.database.query("GRANT SELECT ON ALL TABLES IN SCHEMA public TO PUBLIC") for row in self.database.query("SELECT tablename AS name FROM pg_catalog.pg_tables WHERE schemaname='public' AND tablename LIKE 'kinetics_%'"): if DatabaseInterface._managed_table(row.name, candelete=True): for p in ["DELETE"]: self.database.query("GRANT " + p + " ON " + row.name + ' TO "sysbio"') if row.name.find('kinetics_vo2') == 0: for p in ["INSERT", "UPDATE"]: self.database.query("GRANT " + p + " ON " + row.name + ' TO "respiration"') for p in ["SELECT", "UPDATE"]: self.database.query('GRANT ' + p + ' ON kinetics_experiment_to_mouse_cardiomyocytes_id_seq TO "%s"' % 'sysbio') for t in ["kinetics_roi", "kinetics_experiment", "kinetics_experiment_to_mouse_cardiomyocytes"]: for p in ["SELECT", "INSERT", "UPDATE", "REFERENCES"]: self.database.query('GRANT ' + p + ' ON "%s" TO "%s"' % (t,"sysbio"))
[docs] def grant(self, user): """Grant access permissions for user""" if self.dbtype not in ["postgresql"]: print("Permissions model not supported for", self.dbtype) if self.dbtype == "postgresql": # allow select to PUBLIC self.database.query("GRANT SELECT ON ALL TABLES IN SCHEMA public TO PUBLIC") # # tables # for row in self.database.query("SELECT tablename AS name FROM pg_catalog.pg_tables WHERE schemaname='public'"): # if DatabaseInterface._managed_table(row.name): # for p in ["SELECT"]: # self.database.query("GRANT " + p + " ON " + row.name + ' TO "%s"' % user) for row in self.database.query("SELECT tablename AS name FROM pg_catalog.pg_tables WHERE schemaname='public' AND tablename LIKE 'kinetics_%'"): if DatabaseInterface._managed_table(row.name, candelete=True): for p in ["DELETE"]: self.database.query("GRANT " + p + " ON " + row.name + ' TO "%s"' % user) for p in ["SELECT", "UPDATE"]: self.database.query('GRANT ' + p + ' ON kinetics_experiment_to_mouse_cardiomyocytes_id_seq TO "%s"' % user) for t in ["kinetics_roi", "kinetics_experiment", "kinetics_experiment_to_mouse_cardiomyocytes"]: for p in ["SELECT", "INSERT", "UPDATE", "REFERENCES"]: self.database.query('GRANT ' + p + ' ON "%s" TO "%s"' % (t,user))
# # views # for row in self.database.query("SELECT viewname AS name FROM pg_catalog.pg_views WHERE schemaname='public'"): # if DatabaseInterface._managed_table(row.name): # for p in ["SELECT"]: # self.database.query("GRANT " + p + " ON " + row.name + " TO " + user)
[docs] def revoke(self, user): """Revoke access permissions for user""" if self.dbtype not in ["postgresql"]: print("Permissions model not supported for", self.dbtype) if self.dbtype == "postgresql": # tables for row in self.database.query("SELECT tablename AS name FROM pg_catalog.pg_tables WHERE schemaname='public'"): if DatabaseInterface._managed_table(row.name): for p in ["ALL"]: self.database.query("REVOKE " + p + " ON " + row.name + ' FROM "%s" CASCADE' % user) self.database.query('REVOKE REFERENCES ON kinetics_experiment FROM "%s"' % user) for p in ["ALL"]: self.database.query('REVOKE ' + p + ' ON kinetics_experiment_to_mouse_cardiomyocytes_id_seq FROM "%s"' % user) # views for row in self.database.query("SELECT viewname AS name FROM pg_catalog.pg_views WHERE schemaname='public'"): if DatabaseInterface._managed_table(row.name): for p in ["ALL"]: self.database.query("REVOKE " + p + " ON " + row.name + " FROM " + user + " CASCADE")
##################################################################################### ### Authentication function used by scripts def authenticate_sysbio_database(username, server='sysbio-db.kybi/experiments_v2'): py3 = version_info[0] > 2 # If true python3 is used if py3: cli_input = input else: cli_input = raw_input try: password = keyring.get_password(server, username) except: pwd_file = '/home/martinl/crypt/auth/sysbio-db/pwd' if os.path.isfile(pwd_file): with open(pwd_file, 'r') as f: password = f.read() else: print('No such file:', pwd_file) print('Create password file in crypted folder.') exit() print('Connecting to {}'.format(server)) if password is None: username = cli_input(' Enter your username: ') password = getpass.getpass(prompt=' Enter your password: ') save_inp = cli_input(' Save username and password [n/y]: ') save = False if save_inp == 'y': save = True keyring.set_password(server, username, password) print('Username and password saved.') else: keyring.delete_password(server, username) print('Username and password not saved.') return username, password, server