From 53cea80264458c0359b024967c26c1cc9e19913d Mon Sep 17 00:00:00 2001 From: Antoine Martin Date: Sun, 5 May 2019 07:43:40 +0000 Subject: [PATCH] #2287 add mysql auth git-svn-id: https://xpra.org/svn/Xpra/trunk@22621 3bb7dfac-3a0b-4e04-842a-767bc560f471 --- src/man/xpra.1 | 14 +- src/xpra/server/auth/mysql_auth.py | 89 ++++++++++++ src/xpra/server/auth/sqlauthbase.py | 184 +++++++++++++++++++++++++ src/xpra/server/auth/sqlite_auth.py | 202 ++++++---------------------- 4 files changed, 322 insertions(+), 167 deletions(-) create mode 100755 src/xpra/server/auth/mysql_auth.py create mode 100755 src/xpra/server/auth/sqlauthbase.py diff --git a/src/man/xpra.1 b/src/man/xpra.1 index c0bcf47d58..cf368f6be1 100644 --- a/src/man/xpra.1 +++ b/src/man/xpra.1 @@ -656,6 +656,8 @@ ie: \fB--auth=file:filename=./password.txt\fP. The contents of this file will be treated as binary data, there are no restrictions on character encodings or file size. +Beware of trailing newline characters which will be included in the +password data. .IP \fBmultifile\fP checks the username and password against the file specified using @@ -667,15 +669,17 @@ The file must contain each user credentials on one line of the form: It is not possible to have usernames or password that contain the pipe character \fI|\fP which is used as delimiter, or newlines and carriage returns. +This module is deprecated, \fIsqlite\fP should be used instead. -.IP \fBsqlite\fP +.IP \fBsqlite\fP and \fBmysql\fP checks the username and password against the sqlite database file -specified using the filename option. +specified using the \fIfilename\fP option, or the mysql database +specified using the \fIuri\fP option. The authentication will be processed using the following query -(which is configurable using the "password_query" option): +(which is configurable using the \fIpassword_query\fP option): \fISELECT password FROM users WHERE username=(?)\fP -The sessions available for each user will be querying using: -(this is configurable using the "sessions_query" option): +The sessions available for each user will be queried using: +(this is configurable using the \fIsessions_query\fP option): \fISELECT uid, gid, displays, env_options, session_options FROM users WHERE username=(?)\fP Multiple displays may be specified as a comma separated list. diff --git a/src/xpra/server/auth/mysql_auth.py b/src/xpra/server/auth/mysql_auth.py new file mode 100755 index 0000000000..cf62a2d251 --- /dev/null +++ b/src/xpra/server/auth/mysql_auth.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# This file is part of Xpra. +# Copyright (C) 2019 Antoine Martin +# Xpra is released under the terms of the GNU GPL v2, or, at your option, any +# later version. See the file COPYING for details. + +import re +import sys + +from xpra.server.auth.sys_auth_base import init, log +from xpra.server.auth.sqlauthbase import SQLAuthenticator, DatabaseUtilBase, run_dbutil +assert init and log #tests will disable logging from here + + +def url_path_to_dict(path): + pattern = (r'^' + r'((?P.+?)://)?' + r'((?P.+?)(:(?P.*?))?@)?' + r'(?P.*?)' + r'(:(?P\d+?))?' + r'(?P/.*?)?' + r'(?P[?].*?)?' + r'$' + ) + regex = re.compile(pattern) + m = regex.match(path) + d = m.groupdict() if m is not None else None + return d + +def db_from_uri(uri): + d = url_path_to_dict(uri) + log("settings for uri=%s : %s", uri, d) + import mysql.connector as mysql #@UnresolvedImport + db = mysql.connect( + host = d.get("host", "localhost"), + #port = int(d.get("port", 3306)), + user = d.get("user", ""), + passwd = d.get("password", ""), + database = (d.get("path") or "").lstrip("/") or "xpra", + ) + return db + + +class Authenticator(SQLAuthenticator): + + def __init__(self, username, uri, **kwargs): + SQLAuthenticator.__init__(self, username, **kwargs) + self.uri = uri + + def db_cursor(self, *sqlargs): + db = db_from_uri(self.uri) + cursor = db.cursor() + cursor.execute(*sqlargs) + #keep reference to db so it doesn't get garbage collected just yet: + cursor.db = db + log("db_cursor(%s)=%s", sqlargs, cursor) + return cursor + + def __repr__(self): + return "mysql" + + +class MySQLDatabaseUtil(DatabaseUtilBase): + + def __init__(self, uri): + DatabaseUtilBase.__init__(self, uri) + import mysql.connector as mysql #@UnresolvedImport + assert mysql.paramstyle=="pyformat" + self.param = "%s" + + def exec_database_sql_script(self, cursor_cb, *sqlargs): + db = db_from_uri(self.uri) + cursor = db.cursor() + log("%s.execute%s", cursor, sqlargs) + cursor.execute(*sqlargs) + if cursor_cb: + cursor_cb(cursor) + db.commit() + return cursor + + def get_authenticator_class(self): + return Authenticator + + +def main(): + return run_dbutil(MySQLDatabaseUtil, "databaseURI", sys.argv) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/xpra/server/auth/sqlauthbase.py b/src/xpra/server/auth/sqlauthbase.py new file mode 100755 index 0000000000..70536570e6 --- /dev/null +++ b/src/xpra/server/auth/sqlauthbase.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +# This file is part of Xpra. +# Copyright (C) 2017-2019 Antoine Martin +# Xpra is released under the terms of the GNU GPL v2, or, at your option, any +# later version. See the file COPYING for details. + +from xpra.util import csv, parse_simple_dict +from xpra.os_util import getuid, getgid +from xpra.server.auth.sys_auth_base import SysAuthenticator, init, log +assert init and log #tests will disable logging from here + + +class SQLAuthenticator(SysAuthenticator): + + def __init__(self, username, **kwargs): + self.password_query = kwargs.pop("password_query", "SELECT password FROM users WHERE username=(%s)") + self.sessions_query = kwargs.pop("sessions_query", + "SELECT uid, gid, displays, env_options, session_options "+ + "FROM users WHERE username=(%s) AND password=(%s)") + SysAuthenticator.__init__(self, username, **kwargs) + self.authenticate = self.authenticate_hmac + + def db_cursor(self, *sqlargs): + raise NotImplementedError() + + def get_passwords(self): + cursor = self.db_cursor(self.password_query, (self.username,)) + data = cursor.fetchall() + if not data: + log.info("username '%s' not found in sqlauth database", self.username) + return None + return tuple(str(x[0]) for x in data) + + def get_sessions(self): + cursor = self.db_cursor(self.sessions_query, (self.username, self.password_used or "")) + data = cursor.fetchone() + if not data: + return None + return self.parse_session_data(data) + + def parse_session_data(self, data): + try: + uid = data[0] + gid = data[1] + displays = [] + env_options = {} + session_options = {} + if len(data)>2: + displays = [x.strip() for x in str(data[2]).split(",")] + if len(data)>3: + env_options = parse_simple_dict(str(data[3]), ";") + if len(data)>4: + session_options = parse_simple_dict(str(data[4]), ";") + except Exception as e: + log("parse_session_data() error on row %s", data, exc_info=True) + log.error("Error: sqlauth database row parsing problem:") + log.error(" %s", e) + return None + return uid, gid, displays, env_options, session_options + + +class DatabaseUtilBase(object): + + def __init__(self, uri): + self.uri = uri + self.param = "?" + + def exec_database_sql_script(self, cursor_cb, *sqlargs): + raise NotImplementedError() + + def create(self): + sql = ("CREATE TABLE users (" + "username VARCHAR(255) NOT NULL, " + "password VARCHAR(255), " + "uid VARCHAR(63), " + "gid VARCHAR(63), " + "displays VARCHAR(8191), " + "env_options VARCHAR(8191), " + "session_options VARCHAR(8191))") + self.exec_database_sql_script(None, sql) + + def add_user(self, username, password, uid=getuid(), gid=getgid(), + displays="", env_options="", session_options=""): + sql = "INSERT INTO users(username, password, uid, gid, displays, env_options, session_options) "+\ + "VALUES(%s, %s, %s, %s, %s, %s, %s)" % ((self.param,)*7) + self.exec_database_sql_script(None, sql, + (username, password, uid, gid, displays, env_options, session_options)) + + def remove_user(self, username, password=None): + sql = "DELETE FROM users WHERE username=%s" % self.param + sqlargs = (username, ) + if password: + sql += " AND password=%s" % self.param + sqlargs = (username, password) + self.exec_database_sql_script(None, sql, sqlargs) + + def list_users(self): + fields = ("username", "password", "uid", "gid", "displays", "env_options", "session_options") + def fmt(values, sizes): + s = "" + for i, field in enumerate(values): + if i==0: + s += "|" + s += ("%s" % field).rjust(sizes[i])+"|" + return s + def cursor_callback(cursor): + rows = cursor.fetchall() + if not rows: + print("no rows found") + cursor.close() + return + print("%i rows found:" % len(rows)) + #calculate max size for each field: + sizes = [len(x)+1 for x in fields] + for row in rows: + for i, value in enumerate(row): + sizes[i] = max(sizes[i], len(str(value))+1) + total = sum(sizes)+len(fields)+1 + print("-"*total) + print(fmt((field.replace("_", " ") for field in fields), sizes)) + print("-"*total) + for row in rows: + print(fmt(row, sizes)) + cursor.close() + sql = "SELECT %s FROM users" % csv(fields) + self.exec_database_sql_script(cursor_callback, sql) + + def authenticate(self, username, password): + auth_class = self.get_authenticator_class() + a = auth_class(username, self.uri) + passwords = a.get_passwords() + assert passwords + log("authenticate: got %i passwords", len(passwords)) + assert password in passwords + a.password_used = password + sessions = a.get_sessions() + assert sessions + print("success, found sessions: %s" % (sessions, )) + + def get_authenticator_class(self): + raise NotImplementedError() + + +def run_dbutil(DatabaseUtilClass=DatabaseUtilBase, conn_str="databaseURI", argv=()): + def usage(msg="invalid number of arguments"): + print(msg) + print("usage:") + print(" %s %s create" % (argv[0], conn_str)) + print(" %s %s list" % (argv[0], conn_str)) + print(" %s %s add username password [uid, gid, displays, env_options, session_options]" % (argv[0], conn_str)) + print(" %s %s remove username [password]" % (argv[0], conn_str)) + print(" %s %s authenticate username password" % (argv[0], conn_str)) + return 1 + from xpra.platform import program_context + with program_context("SQL Auth", "SQL Auth"): + l = len(argv) + if l<3: + return usage() + uri = argv[1] + dbutil = DatabaseUtilClass(uri) + cmd = argv[2] + if cmd=="create": + if l!=3: + return usage() + dbutil.create() + elif cmd=="add": + if l<5 or l>10: + return usage() + dbutil.add_user(*argv[3:]) + elif cmd=="remove": + if l not in (4, 5): + return usage() + dbutil.remove_user(*argv[3:]) + elif cmd=="list": + if l!=3: + return usage() + dbutil.list_users() + elif cmd=="authenticate": + if l!=5: + return usage() + dbutil.authenticate(*argv[3:]) + else: + return usage("invalid command '%s'" % cmd) + return 0 diff --git a/src/xpra/server/auth/sqlite_auth.py b/src/xpra/server/auth/sqlite_auth.py index 9ab5ce658a..7b9b64da21 100755 --- a/src/xpra/server/auth/sqlite_auth.py +++ b/src/xpra/server/auth/sqlite_auth.py @@ -4,19 +4,19 @@ # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. -import sys import os +import sys -from xpra.util import parse_simple_dict, csv, engs -from xpra.os_util import getuid, getgid -from xpra.server.auth.sys_auth_base import SysAuthenticator, init, log, parse_uid, parse_gid +from xpra.util import parse_simple_dict +from xpra.server.auth.sys_auth_base import init, log, parse_uid, parse_gid +from xpra.server.auth.sqlauthbase import SQLAuthenticator, DatabaseUtilBase, run_dbutil assert init and log #tests will disable logging from here -class Authenticator(SysAuthenticator): +class Authenticator(SQLAuthenticator): - def __init__(self, username, **kwargs): - filename = kwargs.pop("filename", 'sqlite.sdb') + def __init__(self, username, filename="sqlite.sdb", **kwargs): + SQLAuthenticator.__init__(self, username) if filename and not os.path.isabs(filename): exec_cwd = kwargs.get("exec_cwd", os.getcwd()) filename = os.path.join(exec_cwd, filename) @@ -25,46 +25,24 @@ def __init__(self, username, **kwargs): self.sessions_query = kwargs.pop("sessions_query", "SELECT uid, gid, displays, env_options, session_options "+ "FROM users WHERE username=(?) AND password=(?)") - SysAuthenticator.__init__(self, username, **kwargs) self.authenticate = self.authenticate_hmac def __repr__(self): return "sqlite" - def get_passwords(self): + def db_cursor(self, *sqlargs): if not os.path.exists(self.filename): log.error("Error: sqlauth cannot find the database file '%s'", self.filename) return None - log("sqlauth.get_password() found database file '%s'", self.filename) import sqlite3 - try: - conn = sqlite3.connect(self.filename) - cursor = conn.cursor() - cursor.execute(self.password_query, [self.username]) - data = cursor.fetchall() - except sqlite3.DatabaseError as e: - log("get_password()", exc_info=True) - log.error("Error: sqlauth database access problem:") - log.error(" %s", e) - return None - if not data: - log.info("username '%s' not found in sqlauth database", self.username) - return None - return tuple(str(x[0]) for x in data) + db = sqlite3.connect(self.filename) + db.row_factory = sqlite3.Row + cursor = db.cursor() + cursor.execute(*sqlargs) + log("db_cursor(%s)=%s", sqlargs, cursor) + return cursor - def get_sessions(self): - import sqlite3 - try: - conn = sqlite3.connect(self.filename) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute(self.sessions_query, [self.username, self.password_used or ""]) - data = cursor.fetchone() - except sqlite3.DatabaseError as e: - log("get_sessions()", exc_info=True) - log.error("Error: sqlauth database access problem:") - log.error(" %s", e) - return None + def parse_session_data(self, data): try: uid = parse_uid(data["uid"]) gid = parse_gid(data["gid"]) @@ -72,11 +50,11 @@ def get_sessions(self): env_options = {} session_options = {} if data["displays"]: - displays = [x.strip() for x in str(data[2]).split(",")] + displays = [x.strip() for x in str(data["displays"]).split(",")] if data["env_options"]: - env_options = parse_simple_dict(str(data[3]), ";") + env_options = parse_simple_dict(str(data["env_options"]), ";") if data["session_options"]: - session_options=parse_simple_dict(str(data[4]), ";") + session_options=parse_simple_dict(str(data["session_options"]), ";") except Exception as e: log("get_sessions() error on row %s", data, exc_info=True) log.error("Error: sqlauth database row parsing problem:") @@ -85,131 +63,31 @@ def get_sessions(self): return uid, gid, displays, env_options, session_options -def exec_database_sql_script(cursor_cb, filename, *sqlargs): - log("exec_database_sql_script%s", (cursor_cb, filename, sqlargs)) - import sqlite3 - try: - conn = sqlite3.connect(filename) - cursor = conn.cursor() +class SqliteDatabaseUtil(DatabaseUtilBase): + + def __init__(self, uri): + DatabaseUtilBase.__init__(self, uri) + import sqlite3 + assert sqlite3.paramstyle=="qmark" + self.param = "?" + + def exec_database_sql_script(self, cursor_cb, *sqlargs): + import sqlite3 + db = sqlite3.connect(self.uri) + cursor = db.cursor() + log("%s.execute%s", cursor, sqlargs) cursor.execute(*sqlargs) if cursor_cb: cursor_cb(cursor) - conn.commit() - conn.close() - return 0 - except sqlite3.DatabaseError as e: - log.error("Error: database access problem:") - log.error(" %s", e) - return 1 - - -def create(filename): - if os.path.exists(filename): - log.error("Error: database file '%s' already exists", filename) - return 1 - sql = ("CREATE TABLE users (" - "username VARCHAR NOT NULL, " - "password VARCHAR, " - "uid VARCHAR, " - "gid VARCHAR, " - "displays VARCHAR, " - "env_options VARCHAR, " - "session_options VARCHAR)") - return exec_database_sql_script(None, filename, sql) - -def add_user(filename, username, password, uid=getuid(), gid=getgid(), displays="", env_options="", session_options=""): - sql = "INSERT INTO users(username, password, uid, gid, displays, env_options, session_options) "+\ - "VALUES(?, ?, ?, ?, ?, ?, ?)" - return exec_database_sql_script(None, filename, sql, - (username, password, uid, gid, displays, env_options, session_options)) - -def remove_user(filename, username, password=None): - sql = "DELETE FROM users WHERE username=?" - sqlargs = (username, ) - if password: - sql += " AND password=?" - sqlargs = (username, password) - return exec_database_sql_script(None, filename, sql, sqlargs) - -def list_users(filename): - fields = ("username", "password", "uid", "gid", "displays", "env_options", "session_options") - def fmt(values, sizes): - s = "" - for i, field in enumerate(values): - if i==0: - s += "|" - s += ("%s" % field).rjust(sizes[i])+"|" - return s - def cursor_callback(cursor): - rows = cursor.fetchall() - if not rows: - print("no rows found") - return - print("%i rows found:" % len(rows)) - #calculate max size for each field: - sizes = [len(x)+1 for x in fields] - for row in rows: - for i, value in enumerate(row): - sizes[i] = max(sizes[i], len(str(value))+1) - total = sum(sizes)+len(fields)+1 - print("-"*total) - print(fmt((field.replace("_", " ") for field in fields), sizes)) - print("-"*total) - for row in rows: - print(fmt(row, sizes)) - sql = "SELECT %s FROM users" % csv(fields) - return exec_database_sql_script(cursor_callback, filename, sql) - -def authenticate(filename, username, password): - a = Authenticator(username, filename=filename) - passwords = a.get_passwords() - assert passwords - assert password in passwords - sessions = a.get_sessions() - assert sessions - print("success, found %i session%s: %s" % (len(sessions), engs(sessions), sessions)) - return 0 - -def main(argv): - def usage(msg="invalid number of arguments"): - print(msg) - print("usage:") - print(" %s databasefile create" % sys.argv[0]) - print(" %s databasefile list" % sys.argv[0]) - print(" %s databasefile add username password [uid, gid, displays, env_options, session_options]" % sys.argv[0]) - print(" %s databasefile remove username [password]" % sys.argv[0]) - print(" %s databasefile authenticate username password" % sys.argv[0]) - return 1 - from xpra.platform import program_context - with program_context("SQL Auth", "SQL Auth"): - l = len(argv) - if l<3: - return usage() - filename = argv[1] - cmd = argv[2] - if cmd=="create": - if l!=3: - return usage() - return create(filename) - if cmd=="add": - if l<5 or l>10: - return usage() - return add_user(filename, *argv[3:]) - if cmd=="remove": - if l not in (4, 5): - return usage() - return remove_user(filename, *argv[3:]) - if cmd=="list": - if l!=3: - return usage() - return list_users(filename) - if cmd=="authenticate": - if l!=5: - return usage() - return authenticate(filename, *argv[3:]) - return usage("invalid command '%s'" % cmd) - return 0 + db.commit() + return cursor + + def get_authenticator_class(self): + return Authenticator + +def main(): + return run_dbutil(SqliteDatabaseUtil, "filename", sys.argv) if __name__ == "__main__": - sys.exit(main(sys.argv)) + sys.exit(main())