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