Compare commits

...

15 Commits

Author SHA1 Message Date
Darksider3 f803b1acb4 testcfg: Add genRandomString, which automates away a few cases
... of actual value testing which i won't write manually then! :)

Also implements them on ListUsers
4 years ago
Darksider3 cabe14d41d tests-ListUsers: Remove useless printing to dev/null
Which again allows me to actually debug with print statements.
Dont know which dragon ride gave me the desillusional idea to actually
do it that way
4 years ago
Darksider3 c954940bec tests: Adds verificator-tests & lib: __init__ file
Also more strings to operate on for the tests, obviously.
4 years ago
Darksider3 b4d52b5376 tests: lib.ListUsers tests done
Even more coverage is on it's way. Have still to figure out in which
manner i'm going to test the things i want to.
It's actually not as easy as i expected...
4 years ago
Darksider3 c79edbfc84 tests: backup: Even more! Now with deletion of tested tmp!
Now we also test for everything in backup.py! EVERYTHING!
We even delete the old file that we ourselfs created to test the
export/backup function.
4 years ago
Darksider3 3a6a92267b tests: sample csv files for tests
Enables faster integration of tests.
4 years ago
Darksider3 f201ab603d tests: Add initial infrastructure!
Tests now can be done with `python -m test.test_backup --verbose`!
Im going to add more tests as coverage as soon as i've got time! :)
4 years ago
Darksider3 b8d4dac5da Allow test directories in gitignore 4 years ago
Darksider3 e2c2bcb0c2 import.py: Dont check for users existence while just sanitizing.
This ends the context of commit b4bb45ef85
and 108492db3f with an awesome little
detail, which provides SO MUCH value.
4 years ago
Darksider3 108492db3f private/lib/Validator.py: Further code quality enhancements
Namely:
* f => file_handle
* errstr => err_str
* intendantion adjustements.
* factor out uneccessary if-return-else-return-clause.

Also a new functionality to NOT check for users existence but everything
else. Needed to sanitize in case it's a backup of the current running
instance already. This further adds to commit
b4bb45ef85.
4 years ago
Darksider3 d2f99d95d0 sqlitedb.py: Refactor naming
safequery->safe_query, more snake_case all over the place enforced.
4 years ago
Darksider3 fb620447c7 config_ui: Always point out that the script can operate on the ENV 4 years ago
Darksider3 b4bb45ef85 Import.py: Sanitize given csv in case --Import isnt given and...
... fixup some variable names, also a `pass` that i forget to delete
beforehand.

Also, as you can see in the title, i added the ability to sanitize a
given csv file in case the user didnt supply the --Import flag. Thus, it
isnt required anymore.
4 years ago
Darksider3 aae7504e13 ListUsers.py: Externalize class & rename some variables
This actually makes the whole file easier to read and maintain. I've
still to refactor the output to something.
4 years ago
Darksider3 b5c11455d0 Backup.py -> backup.py, also some PEP conformity-changes
Which also includes two TODO-Tasks.
We had not proven ourselfs worthy of the holy-PEP-construct yet, so we
have to rename the whole file to backu.py, which follows snake_case, and
it's classes members.
They now embrace it as they should've done in the first place!

The dragon which we just encountered isnt as big as we thought and we
can still fight it with our as-easy-as-it-gets task to just snip out the
class and give it it's own residence under lib/backup.py-File!
4 years ago

3
.gitignore vendored

@ -1,4 +1,5 @@
__pycache__/
.idea/
test*
!test/
!test_*.py

@ -1,120 +1,41 @@
#!/usr/bin/env python3
import configparser
import sqlite3 # sqlite3.Row-Object
from typing import List # Typing support!
import lib.uis.default as default_cmd # Follows -u, -a, -f flags
from lib.sqlitedb import SQLiteDB
class ListUsers:
db = None
usersFetch = None
def __init__(self, db: str, unapproved: bool = False, approved: bool = True, single_user: str = None):
"""Constructs ListUsers
:param db: Database to access
:type db: str
:param unapproved: only List unapproved users
:type unapproved: bool
:param approved: only list approved users
:type approved: bool
"""
self.db = SQLiteDB(db)
if unapproved: # only unapproved users
query = "SELECT * FROM `applications` WHERE `status` = '0'"
elif approved: # Approved users
query = "SELECT * FROM `applications` WHERE `status` = '1'"
else: # All users
query = "SELECT * FROM `applications`"
self.usersFetch = self.db.query(query)
if single_user is not None:
query = "SELECT * FROM `applications` WHERE `username` = ?"
self.usersFetch = self.db.safequery(query, tuple([single_user]))
def output_as_list(self) -> str:
"""Generates a string with one (approved) user per line and one newline at the end
:rtype: str
:return: String consisting with one(activated) user per line
"""
list_str: str = ""
query = "SELECT `username` FROM `applications` WHERE `status` = '1' ORDER BY timestamp ASC"
self.usersFetch = self.db.query(query)
for users in self.usersFetch:
list_str += users["username"] + "\n"
return list_str
def prettyPrint(self) -> None:
pass # see below why not implemented yet, texttable...
def get_fetch(self) -> List[sqlite3.Row]:
""" Returns a complete fetch done by the lib.sqlitedb-class
:return: Complete fetchall(). A List[sqlite3.Row] with dict-emulation objects.
:rtype: List[sqlite3.Row]
"""
return self.usersFetch
# @TODO MAYBE best solution: https://pypi.org/project/texttable/
# examle:
"""
from texttable import Texttable
t = Texttable()
t.add_rows([['Name', 'Age'], ['Alice', 24], ['Bob', 19]])
print(t.draw())
---------------> Results in:
+-------+-----+
| Name | Age |
+=======+=====+
| Alice | 24 |
+-------+-----+
| Bob | 19 |
+-------+-----+
from lib.ListUsers import ListUsers
for user in fetch:
print("ID: {}; Username: \"{}\"; Mail: {}; Name: \"{}\"; Registered: {}; Status: {}".format(
user["id"], user["username"], user["email"], user["name"], user["timestamp"], user["status"]
))
"""
if __name__ == "__main__":
default_cmd.argparser.description += " - Lists Users from the Tilde database."
default_cmd.argparser.add_argument('--list-asc', default=False, action="store_true",
help='Output a newline seperated list of users', required=False, dest="args_asc")
default_cmd.argparser.add_argument('--user', default=None, type=str,
help="Just show a specific user by it's name", required=False)
default_cmd.argparser.add_argument('--single_user', default=None, type=str,
help="Just show a specific single_user by it's name", required=False)
args = default_cmd.argparser.parse_args()
config = configparser.ConfigParser()
config.read(args.config)
ret = ""
if args.user is not None:
OUTPUT = ""
if args.single_user is not None:
L = ListUsers(config['DEFAULT']['applications_db'], unapproved=args.unapproved, approved=args.approved,
single_user=args.user)
single_user=args.single_user)
else:
L = ListUsers(config['DEFAULT']['applications_db'], unapproved=args.unapproved, approved=args.approved)
if args.args_asc:
ret = L.output_as_list()
OUTPUT = L.output_as_list()
else:
fetch = L.get_fetch()
ret += "ID %-1s| Username %-5s| Mail %-20s| Name %-17s| Registered %-8s | State |\n" % (
users = L.get_fetch()
OUTPUT += "ID %-1s| Username %-5s| Mail %-20s| Name %-17s| Registered %-8s | State |\n" % (
" ", " ", " ", " ", " "
)
ret += 102 * "-" + "\n"
for user in fetch:
ret += "%-4i| %-14s| %-25s| %-22s| %-8s | %-5i |\n" % (
user["id"], user["username"], user["email"], user["name"], user["timestamp"], user["status"]
OUTPUT += 102 * "-" + "\n"
for single_user in users:
OUTPUT += "%-4i| %-14s| %-25s| %-22s| %-8s | %-5i |\n" % (
single_user["id"], single_user["username"], single_user["email"],
single_user["name"], single_user["timestamp"], single_user["status"]
)
if args.file != "stdout":
with open(args.file, 'w') as f:
print(ret, file=f)
print(OUTPUT, file=f)
else:
print(ret)
print(OUTPUT)
exit(0)

@ -1,20 +1,26 @@
#!/usr/bin/env python3
""" This module is thought to be the main point to export and import users.
It's actually not really a module but a script ought to be run from the command line
@TODO: Wording of module header...
"""
import configparser
import csv
import io
import ListUsers
from lib.ListUsers import ListUsers
import lib.uis.default as default_cmd # Follows -u, -a, -f flags
class Backup:
"""Backups a Tilde database to an CSV file
@TODO: Move class into own file
:Example:
>>> from Backup import Backup
>>> from backup import Backup
>>> from ListUsers import ListUsers
>>> L = ListUsers.ListUsers("/path/to/sqlite").get_fetch()
>>> L = ListUsers.list_users("/path/to/sqlite").get_fetch()
>>> backup_db = Backup("stdout")
>>> backup_db.backup_to_file(L)
CSV-Separated list with headers in first row
@ -37,12 +43,13 @@ class Backup:
:type dialect: str
"""
self.setFilename(output)
self.setQuoting(quoting)
self.setDialect(dialect)
self.setFieldnames(tuple(['id', 'username', 'email', 'name', 'pubkey', 'timestamp', 'status']))
self.set_filename(output)
self.set_quoting(quoting)
self.set_dialect(dialect)
self.set_field_names(tuple(['id', 'username', 'email', 'name',
'pubkey', 'timestamp', 'status']))
def setDialect(self, dialect: str) -> None:
def set_dialect(self, dialect: str) -> None:
""" Set dialect for Object
:param dialect: Dialect to set for Object
@ -53,7 +60,7 @@ class Backup:
self.dialect = dialect
def setQuoting(self, quoting: int) -> None:
def set_quoting(self, quoting: int) -> None:
""" Set quoting in the CSV(must be supported by the CSV Module!)
:param quoting: Quoting Integer given by csv.QUOTE_* constants
@ -64,10 +71,10 @@ class Backup:
self.quoting = quoting
def setFilename(self, filename: str) -> None:
""" Sets Filename to output to
def set_filename(self, filename: str) -> None:
""" Sets Filename to OUTPUT to
:param filename: Filename to output to(set stdout for stdout)
:param filename: Filename to OUTPUT to(set stdout for stdout)
:type filename: str
:return: None
:rtype: None
@ -75,8 +82,8 @@ class Backup:
self.filename = filename
def setFieldnames(self, f_names: tuple) -> None:
""" Set fieldname to process
def set_field_names(self, f_names: tuple) -> None:
""" Set field name to process
:param f_names: Fieldnames-Tuple
:type f_names: tuple
@ -95,20 +102,22 @@ class Backup:
"""
returner = io.StringIO()
write_csv = csv.DictWriter(returner, fieldnames=self.field_names, quoting=self.quoting, dialect=self.dialect)
write_csv = csv.DictWriter(returner, fieldnames=self.field_names,
quoting=self.quoting, dialect=self.dialect)
write_csv.writeheader()
for row in fetched:
write_csv.writerow(dict(row))
# sqlite3.Row doesn't "easily" convert to a dict itself sadly, so just a quick help from us here
# it actually even delivers a list(sqlite3.Row) also, which doesnt make the life a whole lot easier
# sqlite3.Row doesn't "easily" convert to a dict itself sadly,
# so just a quick help from us here
# it actually even delivers a list(sqlite3.Row) also,
# which doesnt make the life a whole lot easier
if self.filename == "stdout":
print(returner.getvalue())
return True
else:
with open(self.filename, "w") as f:
print(returner.getvalue(), file=f)
return True
return True
if __name__ == "__main__":
@ -116,12 +125,12 @@ if __name__ == "__main__":
args = default_cmd.argparser.parse_args()
config = configparser.ConfigParser()
config.read(args.config)
L = ListUsers.ListUsers(config['DEFAULT']['applications_db'],
unapproved=args.unapproved, approved=args.approved)
L = ListUsers(config['DEFAULT']['applications_db'],
unapproved=args.unapproved, approved=args.approved)
fetch = L.get_fetch()
if fetch:
B = Backup(args.file)
B.setFieldnames(fetch[0].keys()) # sqlite3.row delivers its keys for us! SO NICE!
B.set_field_names(fetch[0].keys()) # sqlite3.row delivers its keys for us! SO NICE!
B.backup_to_file(fetch)
else:
print("nothing to backup!")

@ -50,7 +50,7 @@ if __name__ == "__main__":
if not DB:
print("Could not establish connection to database")
exit(1)
CurrentUser = DB.safequery("SELECT * FROM `applications` WHERE `username`=?", tuple([args.user]))[0]
CurrentUser = DB.safe_query("SELECT * FROM `applications` WHERE `username`=?", tuple([args.user]))[0]
# --> --remove
if args.remove:
@ -75,9 +75,9 @@ if __name__ == "__main__":
print(f"Pubkey '{args.sshpubkey}' isn't valid.")
exit(1)
try:
DB.safequery("UPDATE `applications` SET `pubkey`=? WHERE `username`=?",
tuple([args.sshpubkey, args.user]))
CurrentUser = DB.safequery("SELECT * FROM `applications` WHERE `username` = ? ", tuple([args.user]))[0]
DB.safe_query("UPDATE `applications` SET `pubkey`=? WHERE `username`=?",
tuple([args.sshpubkey, args.user]))
CurrentUser = DB.safe_query("SELECT * FROM `applications` WHERE `username` = ? ", tuple([args.user]))[0]
if int(CurrentUser["status"]) == 1:
sys_ctl.make_ssh_usable(args.sshpubkey)
except sqlite3.Error as e:
@ -93,7 +93,7 @@ if __name__ == "__main__":
print(f"'{args.name}' is not a valid Name.")
exit(1)
try:
DB.safequery("UPDATE `applications` SET `name` =? WHERE `username` =?", tuple([args.name, args.user]))
DB.safe_query("UPDATE `applications` SET `name` =? WHERE `username` =?", tuple([args.name, args.user]))
except sqlite3.Error as e:
print(f"Could not write '{args.name}' to database: {e}")
print(f"'{args.user}'s Name changed to '{args.name}'.")
@ -104,7 +104,7 @@ if __name__ == "__main__":
print(f"'{args.email}' is not a valid Mail address!")
exit(1)
try:
DB.safequery("UPDATE `applications` SET `email` =? WHERE `username` =?", tuple([args.email]))
DB.safe_query("UPDATE `applications` SET `email` =? WHERE `username` =?", tuple([args.email]))
except sqlite3.Error as e:
print(f"Could not write '{args.email}' to the database. {e}")
print(f"'{args.user}' Mail changed to '{args.email}'.")
@ -121,8 +121,8 @@ if __name__ == "__main__":
if args.status == 0 and int(CurrentUser["status"]) == 1:
try:
DB.safequery("UPDATE `applications` SET `status` =? WHERE `id`=?",
tuple([args.status, CurrentUser["id"]]))
DB.safe_query("UPDATE `applications` SET `status` =? WHERE `id`=?",
tuple([args.status, CurrentUser["id"]]))
sys_ctl.remove_user()
except sqlite3.Error as e:
print(f"Could not update database entry for '{args.user}', did not touch the system")
@ -134,8 +134,8 @@ if __name__ == "__main__":
if args.status == 1 and int(CurrentUser["status"]) == 0:
try:
DB.safequery("UPDATE `applications` SET `status`=? WHERE `username`=?",
tuple([args.status, args.user]))
DB.safe_query("UPDATE `applications` SET `status`=? WHERE `username`=?",
tuple([args.status, args.user]))
sys_ctl.aio_approve(CurrentUser["pubkey"])
except sqlite3.Error as e:
print(f"Could not update Users status in database")

@ -1,14 +1,17 @@
#!/usr/bin/env python3
"""
Import users or sanitize backup files
"""
import configparser
import csv
import os
import lib.UserExceptions
import lib.uis.config_ui # dont go to default, just following -c flag
import lib.Validator
def import_from_file(file_path: str, db: str, user_ids: tuple = tuple([])) -> bool:
def import_from_file(file_path: str, database_file: str, user_ids: tuple = tuple([])) -> bool:
""" Imports Users from a given CSV-file to the system and DB
:param file_path:
@ -23,22 +26,21 @@ def import_from_file(file_path: str, db: str, user_ids: tuple = tuple([])) -> bo
if not os.path.isfile(file_path):
print(f"File {file_path} don't exist")
return False
if not os.path.isfile(db):
print(f"The database file {db} don't exist")
if not os.path.isfile(database_file):
print(f"The database file {database_file} don't exist")
return False
if user_ids:
pass # empty tuple means everything
# noinspection PyBroadException
try:
with open(file_path, 'r', newline='') as f:
import lib.Validator
sql = lib.sqlitedb.SQLiteDB(db)
err = lib.Validator.checkImportFile(file_path, db)
import lib.sqlitedb
import lib.System
sql = lib.sqlitedb.SQLiteDB(database_file)
err = lib.Validator.checkImportFile(file_path, database_file)
if err is not True:
print(err)
exit(0)
import lib.sqlitedb
import lib.System
sys_ctl = lib.System.System("root")
reader = csv.DictReader(f) # @TODO csv.Sniffer to compare? When yes, give force-accept option
for row in reader:
@ -47,20 +49,19 @@ def import_from_file(file_path: str, db: str, user_ids: tuple = tuple([])) -> bo
sys_ctl.setUser(row["username"])
sys_ctl.aio_approve(row["pubkey"])
print(row['username'], "====> Registered.")
except lib.UserExceptions.General as GeneralExcept:
print(f"Something didnt work out! {GeneralExcept}")
except lib.UserExceptions.General as general_except:
print(f"Something didnt work out! {general_except}")
elif row["status"] == "0":
print(row['username'] + " not approved, therefore not registered.")
try:
sql.safequery(
sql.safe_query(
"INSERT INTO `applications` (username, name, timestamp, email, pubkey, status) "
"VALUES (?,?,?,?,?,?)", tuple([row["username"], row["name"], row["timestamp"],
row["email"], row["pubkey"], row["status"]]))
except OSError as E:
pass
print(f"UUFFF, something went WRONG with the file {file_path}: {E}")
except Exception as didntCatch:
print(f"Exception! UNCATCHED! {type(didntCatch)}: {didntCatch}")
except OSError as os_exception:
print(f"UUFFF, something went WRONG with the file {file_path}: {os_exception}")
except Exception as didnt_catch:
print(f"Exception! UNCATCHED! {type(didnt_catch)}: {didnt_catch}")
return True
@ -71,17 +72,24 @@ if __name__ == "__main__":
ArgParser.add_argument('-f', '--file', default="stdout",
type=str, help='Import from CSV file', required=True)
ArgParser.add_argument('--Import', default=False, action="store_true",
help="Import Users.", required=True)
help="Import Users. If not set, just sanitize the supplied csv", required=False)
args = ArgParser.parse_args()
config = configparser.ConfigParser()
config.read(args.config)
if not args.Import:
print("Error, need the import flag")
if not args.file:
print("Error, need the import file")
if not args.file:
print("You MUST set a CSV-file with the -f/--file flag that already exist")
if not args.Import:
# we assume that you just want to sanitize the backup
if not os.path.isfile(args.file):
print(f"File {args.file} doesnt exist")
exit(1)
import_from_file(args.file, config['DEFAULT']['applications_db'])
sanitized = lib.Validator.checkImportFile(args.file, config['DEFAULT']['applications_db'], False)
if sanitized is not True:
print(sanitized)
else:
print(f"{args.file} is valid!")
exit(0)
elif args.Import:
import_from_file(args.file, config['DEFAULT']['applications_db'])
exit(0)

@ -0,0 +1,65 @@
from lib.sqlitedb import SQLiteDB
import sqlite3 # sqlite3.Row-Object
from typing import List # Typing support!
class ListUsers:
"""
List tilde users
"""
db = None
users_fetch = None
def __init__(self, db: str, unapproved: bool = False, approved: bool = True, single_user: str = None):
"""Constructs list_users
:param db: Database to access
:type db: str
:param unapproved: only List unapproved users
:type unapproved: bool
:param approved: only list approved users
:type approved: bool
"""
self.db = SQLiteDB(db)
if unapproved: # only unapproved users
query = "SELECT * FROM `applications` WHERE `status` = '0'"
elif approved: # Approved users
query = "SELECT * FROM `applications` WHERE `status` = '1'"
else: # All users
query = "SELECT * FROM `applications`"
self.users_fetch = self.db.query(query)
if single_user is not None:
query = "SELECT * FROM `applications` WHERE `username` = ?"
self.users_fetch = self.db.safe_query(query, tuple([single_user]))
def output_as_list(self) -> str:
"""Generates a string with one (approved) single_user per line and one newline at the end
:rtype: str
:return: String consisting with one(activated) single_user per line
"""
list_str: str = ""
query = "SELECT `username` FROM `applications` WHERE `status` = '1' ORDER BY timestamp ASC"
self.users_fetch = self.db.query(query)
for user in self.users_fetch:
list_str += user["username"] + "\n"
return list_str
def pretty_print(self) -> None:
"""
pretty-print users
:return: None
"""
pass # see below why not implemented yet, texttable...
def get_fetch(self) -> List[sqlite3.Row]:
""" Returns a complete users done by the lib.sqlitedb-class
:return: Complete fetchall(). A List[sqlite3.Row] with dict-emulation objects.
:rtype: List[sqlite3.Row]
"""
return self.users_fetch

@ -72,7 +72,7 @@ def checkUserInDB(username: str, db: str) -> bool:
try:
ldb = lib.sqlitedb.SQLiteDB(db)
fetched = ldb.safequery("SELECT * FROM 'applications' WHERE username = ?", tuple([username]))
fetched = ldb.safe_query("SELECT * FROM 'applications' WHERE username = ?", tuple([username]))
if fetched:
return True
except lib.sqlitedb.sqlite3.Error as e:
@ -156,7 +156,7 @@ def checkName(name: str) -> bool:
return True
def checkImportFile(path: str, db: str):
def checkImportFile(path: str, db: str, test_existence: bool = True):
""" Checks an CSV file against most of the validators and prints an Error message with the line number corresponding
to said failure.. Those includes: checkName, checkUsernameCharacters,
ckeckUsernameLength, duplicate usernames(in the CSV), checkSSHKey, checkEmail, checkUserExists, checkUserInDB,
@ -166,54 +166,64 @@ def checkImportFile(path: str, db: str):
:type path: str
:param db: Path to database file(SQLite)
:type db: str
:param test_existence: Flag, checking users existence while true, won't when set to false. Default's to true.
:type test_existence: bool
:return: Str when Failure, True when success(All tests passed)
:rtype: Str or None
"""
errstr = ""
err_str = ""
valid = True
ln = 1 # line number
line = 1 # line number
valid_names_list = []
with open(path, 'r', newline='') as f:
reader = csv.DictReader(f)
with open(path, 'r', newline='') as file_handle:
reader = csv.DictReader(file_handle)
for row in reader:
# if any of this fails move on to the next user, just print a relatively helpful message lel
if not lib.Validator.checkName(row["name"]):
errstr += f"Line {ln}: Name: '{row['name']}' seems not legit. Character followed by character should" \
f" be correct.\n"
err_str += f"Line {line}: Name: '{row['name']}' seems not legit. " \
f"Character followed by character should be correct.\n"
valid = False
if not lib.Validator.checkUsernameCharacters(row["username"]):
errstr += (f"Line {ln}: Username contains unsupported characters or starts with a number: '"
f"{row['username']}'.\n")
err_str += (f"Line {line}: "
f"Username contains unsupported characters or starts with a number: '"
f"{row['username']}'.\n")
valid = False
if not lib.Validator.checkUsernameLength(row["username"]):
errstr += f"Line {ln}: Username '{row['username']}' is either too long(>16) or short(<3)\n"
err_str += f"Line {line}: " \
f"Username '{row['username']}' is either too long(>16) or short(<3)\n"
valid = False
# dup checking
if row["username"] in valid_names_list:
errstr += f"Line {ln}: Duplicate Username {row['username']}!\n"
err_str += f"Line {line}: Duplicate Username {row['username']}!\n"
valid = False
else:
valid_names_list.append(row["username"])
# dup end
if not lib.Validator.checkSSHKey(row["pubkey"]):
errstr += f"Line {ln}: Following SSH-Key of user '{row['username']}' isn't valid: " \
f"'{row['pubkey']}'.\n"
err_str += f"Line {line}: " \
f"Following SSH-Key of user '{row['username']}' isn't valid: " \
f"'{row['pubkey']}'.\n"
valid = False
if not lib.Validator.checkEmail(row["email"]):
errstr += f"Line {ln}: E-Mail address of user '{row['username']}' '{row['email']}' is not valid.\n"
err_str += \
f"Line {line}: " \
f"E-Mail address of user '{row['username']}' '{row['email']}' is not valid.\n"
valid = False
if lib.Validator.checkUserExists(row["username"]) or checkUserInDB(row["username"], db):
errstr += f"Line {ln}: User '{row['username']}' already exists.\n"
valid = False
if test_existence:
err_str += f"Line {line}: User '{row['username']}' already exists.\n"
valid = False
else:
pass
if not lib.Validator.checkDatetimeFormat(row["timestamp"]):
errstr += f"Line {ln}: Timestamp '{row['timestamp']}' from user '{row['username']}' is invalid.\n"
err_str += f"Line {line}: Timestamp '{row['timestamp']}' " \
f"from user '{row['username']}' is invalid.\n"
valid = False
if int(row["status"]) > 1 or int(row["status"]) < 0:
errstr += f"Line {ln}: Status '{row['status']}' MUST be either 0 or 1.\n"
err_str += f"Line {line}: Status '{row['status']}' MUST be either 0 or 1.\n"
valid = False
ln += 1
line += 1
if valid:
return True
else:
return errstr
err_str = True
return err_str

@ -1,4 +1,8 @@
#!/usr/bin/env python3
"""
SQLite wrapper which does just some simple wraps, to ease our experience a little.
"""
import sqlite3
from sys import stderr as stderr
from typing import List # Typing support!
@ -20,20 +24,18 @@ class SQLiteDB:
connection = None
last_result = None
def __init__(self, dbpath: str):
def __init__(self, db_path: str):
"""
:param dbpath: Path to the database we want to open
:type dbpath: str
:param db_path: Path to the database we want to open
:type db_path: str
:returns: Object for the SQLitedb-Class.
:rtype: object
"""
db = dbpath
try:
self.connection = sqlite3.connect(db)
self.connection = sqlite3.connect(db_path)
self.cursor = self.connection.cursor()
except sqlite3.Error as e:
print("Connection error: %s" % e, file=stderr)
except sqlite3.Error as sql_con:
print("Connection error: %s" % sql_con, file=stderr)
self.cursor.row_factory = sqlite3.Row # every result will be a dict now
@ -44,38 +46,39 @@ class SQLiteDB:
except sqlite3.Error as e:
print("Couldn't gracefully close db: %s" % e, file=stderr)
def query(self, qq: str) -> List[sqlite3.Row]:
def query(self, q_str: str) -> List[sqlite3.Row]:
"""Do a query and automagically get the fetched results in a list
:param qq: Query to execute
:type qq: str
:param q_str: Query to execute
:type q_str: str
:returns: A tuple(/list) consisting with any fetched results
:rtype: list
"""
try:
self.cursor.execute(qq)
self.cursor.execute(q_str)
self.last_result = self.cursor.fetchall()
self.connection.commit()
except sqlite3.OperationalError:
self._createTable()
return self.query(qq)
except sqlite3.Error as e:
print("Couldn't execute query %s, exception: %s" % (qq, e), file=stderr)
return self.query(q_str)
except sqlite3.Error as sql_query_except:
print("Couldn't execute query %s, exception: %s" % (q_str, sql_query_except),
file=stderr)
self.last_result = []
return self.last_result
# sometimes we need the cursor for safety reasons, for example does sqlite3 all the security related
# escaoing in supplied strings for us, when we deliver it to con.execute in the second argument as a tuple
def getCursor(self) -> sqlite3:
def get_cursor(self) -> sqlite3:
"""Returns SQLite3 Cursor. Use with **c a u t i o n**... """
return self.cursor
# we could try to utilise that ourselfs in a function. Be c a r e f u l, these values in the tuple MUST HAVE
# THE RIGHT TYPE
def safequery(self, qq: str, deliver: tuple) -> List[sqlite3.Row]:
def safe_query(self, q_str: str, deliver: tuple) -> List[sqlite3.Row]:
""" Shall handle any query that has user input in it as an alternative to self.query
:param qq: Query to execute
:type qq: str
:param q_str: Query to execute
:type q_str: str
:param deliver: User inputs marked with the placeholder(`?`) in the str
:type deliver: tuple
:returns: A tuple(/list) consisting with any fetched results
@ -83,39 +86,40 @@ class SQLiteDB:
"""
try:
self.cursor.execute(qq, deliver)
self.cursor.execute(q_str, deliver)
self.last_result = self.cursor.fetchall()
self.connection.commit()
except TypeError as e:
print("Types in given tuple doesnt match to execute query \"%s\": %s" % (qq, e), file=stderr)
except TypeError as type_err:
print("Types in given tuple doesnt match to execute query \"%s\": %s" % (q_str, type_err), file=stderr)
self.last_result = []
except sqlite3.OperationalError:
self._createTable()
return self.safequery(qq, deliver)
except sqlite3.Error as e:
print("Couldn't execute query %s, exception: %s" % (qq, e), file=stderr)
return self.safe_query(q_str, deliver)
except sqlite3.Error as sql_query_error:
print("Couldn't execute query %s, exception: %s" % (q_str, sql_query_error), file=stderr)
print(deliver)
print(type(e))
print(type(sql_query_error))
self.last_result = []
return self.last_result
def removeApplicantFromDB(self, userid: int) -> bool:
def removeApplicantFromDB(self, user_id: int) -> bool:
"""Removes Applicants from the DB by ID. Use along System.removeUser()
:param userid: User ID to remove from the Database
:type userid: int
:param user_id: User ID to remove from the Database
:type user_id: int
:returns: True, if removal was successful(from the DB), False when not
:rtype: bool
"""
try:
self.last_result = self.cursor.execute("DELETE FROM `applications` WHERE id = ? ", [userid])
self.last_result = self.cursor.execute("DELETE FROM `applications` WHERE id = ? ",
[user_id])
self.connection.commit()
except sqlite3.OperationalError:
print("The database has probably not yet seen any users, so it didnt create your table yet. Come back"
"when a user tried to register")
return False
except sqlite3.Error as e:
print(f"Could not delete user with id: {userid}, exception in DB: {e}") # @TODO LOGGING FFS
except sqlite3.Error as query_error:
print(f"Could not delete user with id: {user_id}, exception in DB: {query_error}") # @TODO LOGGING FFS
return False
return True
@ -134,8 +138,8 @@ class SQLiteDB:
print("The database has probably not yet seen any users, so it didnt create your table yet. Come back"
"when a user tried to register")
return False
except sqlite3.Error as e:
print(f"Could not delete user {username}, exception in DB: {e}") # @TODO LOGGING
except sqlite3.Error as sql_error:
print(f"Could not delete user {username}, exception in DB: {sql_error}") # @TODO LOGGING
return False
return True
@ -150,8 +154,8 @@ class SQLiteDB:
"timestamp_valid CHECK( timestamp IS strftime('%Y-%m-%d %H:%M:%S', timestamp))"
",status INTEGER NOT NULL DEFAULT 0);")
self.connection.commit()
except sqlite3.Error as e:
print(f"The database probably doesn't exist yet, but read the message: {e}")
except sqlite3.Error as sql_error:
print(f"The database probably doesn't exist yet, but read the message: {sql_error}")
print("The database table didn't exist yet; created it successfully!")

@ -1,6 +1,9 @@
import argparse
import lib.cwd
argparser = argparse.ArgumentParser(description='Tilde administration tools ', conflict_handler="resolve")
argparser = argparse.ArgumentParser(description='Tilde administration tools ',
conflict_handler="resolve")
argparser.add_argument('-c', '--config', default=lib.cwd.cwd,
type=str, help='Path to configuration file', required=False)
type=str,
help='Path to configuration file. If not set, we look for it in $TILDE_CONF',
required=False)

Binary file not shown.

@ -0,0 +1,47 @@
import unittest
import test.testcfg as testcfg
import sys
import os
from lib.ListUsers import ListUsers
sys.path.append('..')
class TestListUsers(unittest.TestCase):
def setUp(self) -> None:
try:
self.list = ListUsers(testcfg.test_db, unapproved=False, approved=False)
except IOError as fs_err:
self.fail(fs_err)
def test_singleUser(self):
try:
ListUsers(testcfg.test_db, single_user=testcfg.assured_user)
except Exception:
self.fail()
def test_output_as_list(self):
# count newlines in string, everything under 3 is wrong, and above 20 too.
out = self.list.output_as_list().splitlines()
self.assertGreater(len(out), testcfg.ListUsers_output_newlines,
"Newlines in OUTPUT doesn't exceed minimum of at least "
f"{testcfg.ListUsers_output_newlines} "
"lines!")
def test_pretty_print(self):
# wont going to compare the stdout sorry very much
return
def test_get_fetch(self):
fetch = self.list.get_fetch()
self.assertIsInstance(fetch, list)
self.assertGreater(len(fetch),
testcfg.ListUsers_fetch_size_min, "fetch is NOT greater than"
"the configured fetch minimum")
try:
fetch[0]
except (KeyError, IOError) as suddenly_not_there:
self.fail(f"Expected fetch to have at least one argument! {suddenly_not_there}")
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,76 @@
import sys
sys.path.append('..')
import unittest
import lib.Validator
import test.testcfg as testcfg
class TestValidator(unittest.TestCase):
def setUp(self) -> None:
self.validator = lib.Validator
def test_check_username_characters(self):
for name in testcfg.Validator_Valid_Users_Chars_List:
self.assertTrue(self.validator.checkUsernameCharacters(name))
for name in testcfg.Validator_Invalid_Users_Chars_List:
self.assertFalse(self.validator.checkUsernameCharacters(name), name)
for name in testcfg.genRandomString():
self.assertFalse(self.validator.checkUsernameCharacters(name), name)
def test_check_username_length(self):
for name in testcfg.Validator_Valid_Users_Length:
self.assertTrue(self.validator.checkUsernameLength(name))
for name in testcfg.Validator_Invalid_Users_Length:
self.assertFalse(self.validator.checkUsernameLength(name), name)
def test_check_email(self):
for name in testcfg.Validator_Valid_Mail:
self.assertTrue(self.validator.checkEmail(name))
for name in testcfg.Validator_Invalid_Mail:
self.assertFalse(self.validator.checkEmail(name), name)
for name in testcfg.genRandomString():
self.assertFalse(self.validator.checkEmail(name))
def test_check_user_exists(self):
self.assertTrue(self.validator.checkUserExists("root"))
self.assertTrue(self.validator.checkUserExists("nobody")) # remove in case there exists an actual system without
for User in testcfg.genRandomString():
self.assertFalse(self.validator.checkUserExists(User))
def test_check_user_in_db(self):
for name in testcfg.Validator_db_user_exists:
self.assertTrue(self.validator.checkUserInDB(name, testcfg.test_db))
for name in testcfg.Validator_db_user_inexistent:
self.assertFalse(self.validator.checkUserInDB(name, testcfg.test_db))
def test_check_sshkey(self):
for key in testcfg.Validator_valid_ssh:
self.assertTrue(self.validator.checkSSHKey(key))
for key in testcfg.Validator_invalid_ssh:
self.assertFalse(self.validator.checkSSHKey(key))
def test_check_datetime_format(self):
for cur in testcfg.Validator_valid_datetime:
self.assertTrue(self.validator.checkDatetimeFormat(cur))
for cur in testcfg.Validator_invalid_checkname_names:
self.assertFalse(self.validator.checkDatetimeFormat(cur))
for cur in testcfg.genRandomString():
self.assertFalse(self.validator.checkDatetimeFormat(cur))
def test_check_name(self):
for name in testcfg.Validator_valid_checkname_names:
self.assertTrue(self.validator.checkName(name))
for name in testcfg.Validator_invalid_checkname_names:
self.assertFalse(self.validator.checkName(name))
for name in testcfg.genRandomString(alphabet="\n\b\r\t\f"):
self.assertFalse(self.validator.checkName(name))
def test_check_import_file(self):
self.assertTrue(self.validator.checkImportFile(testcfg.test_import_csv,
testcfg.test_db))
if not self.validator.checkImportFile(testcfg.test_import_invalid_csv, testcfg.test_db):
self.fail("Invalid import file should've failed the test")

@ -0,0 +1,50 @@
import os
import unittest
import sys
import csv
import backup
sys.path.append('..')
from lib.ListUsers import ListUsers
import test.testcfg as testcfg
class TestBackup(unittest.TestCase):
def setUp(self):
try:
self.fetch = ListUsers(testcfg.test_db, unapproved=False, approved=False).get_fetch()
self.Backup = backup.Backup(testcfg.test_backup_csv)
except Exception as general_setup:
self.fail(f"Failed setup already! {general_setup}")
def test_set_dialect(self):
self.Backup.set_dialect("excel")
self.assertEqual(self.Backup.dialect, "excel")
def test_set_quoting(self):
self.Backup.set_quoting(csv.QUOTE_NONNUMERIC)
self.assertEqual(self.Backup.quoting, csv.QUOTE_NONNUMERIC)
def test_set_filename(self):
self.Backup.set_filename(testcfg.test_backup_csv)
self.assertEqual(self.Backup.filename, testcfg.test_backup_csv)
self.Backup.set_field_names(self.fetch[0].keys())
def test_set_field_names(self):
# @TODO: Dynamic! Having a test scheme from which we setup our test is beneficial here, also values
self.Backup.set_field_names(self.fetch[0].keys())
keys_found = self.Backup.field_names
self.assertEqual(keys_found, ['id', 'username', 'email', 'name', 'pubkey', 'timestamp', 'status'])
def test_backup_to_file(self):
try:
self.Backup.set_field_names(self.fetch[0].keys())
self.Backup.backup_to_file(self.fetch)
self.assertTrue(os.path.exists(testcfg.test_backup_csv),
"Assert True that file exists and was written")
os.unlink(os.path.realpath(testcfg.test_backup_csv))
except IOError as io_error:
self.fail(io_error)
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,60 @@
test_random_iterations: int = 50
def genRandomString(iterations: int = test_random_iterations, size: int = 20, alphabet: str = ""):
import random, string
if alphabet == "":
letters = string.digits + string.printable + string.ascii_letters
else:
letters = alphabet
for i in range(iterations):
yield ''.join(random.choice(letters) for i in range(size)).capitalize()
assured_user: str = "darksider3"
test_backup_csv: str = "test/testbackup.csv"
test_db: str = "./test/applications.sqlite"
test_import_csv: str = "test/testimport.csv"
test_import_invalid_csv: str = "test/testimport_fail.csv"
ListUsers_fetch_size_min: int = 3
ListUsers_output_newlines: int = 1
Validator_Valid_Users_Chars_List: list = ["darksider", "dirty", "hAndS", "world312Lol"]
Validator_Invalid_Users_Chars_List: list = ["12", "#uß", "Rawr"]
Validator_Valid_Users_Length: list = ["w💁💁💞💞elt", "hallo", "japp", "eksdee", "harrharr", "räraRüdigerSauß하",
"التَّطْبِيقَاتُ",
"𝓽𝓱𝓮", "𝓵𝓪𝔃𝔂", "𝓭𝓸𝓰"]
Validator_Invalid_Users_Length: list = ["f", "i", "fa", "fo", "ar"]
Validator_Valid_Mail: list = ["larp@example.org", "rawr@lulz.com", "woerld@hassa.fun"]
Validator_Invalid_Mail: list = ["läÄ@wi", "@rawr.", ".com", "@.de", ".@.de"]
Validator_db_user_exists: list = ["darksider3", "Darksider2", "Darksider1"]
Validator_db_user_inexistent: list = ["welt", "world", "hä#", "root"]
# @TODO: More valid and invalid test keys...
Validator_valid_ssh: list = ["""ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC7qmegDxzv1omqG2cWM+
i+qaEGzCoSBwqCeXyGUU93sTqtNYYHJVGj6YZqXeXEGzJtKm2A/uo59Y+WmqhJgW7HcT2Hqvo80NfbIRhqE9TJETyBe
GiiC8qpiYgPC2zigCNvTsRXh0CH5FJ1qy4QEBjztQDWOqSrsoOSJEEWCJiKJizTiXDmlGdiKE409GBo8lvlbMRWbrMj
3iX825WTqy/T0Pio1kqANDotLnPA0sRXUPVyzc/ghzqRHzFetzP9j7C0nh
EvjiJphiuYvhbgix79FrCQG0lXBGcAWzsWUeAoT/d3kQu79+UTWxm+z4pnJ7gkKVMejqrWys560SdAqD264dc5UBRGI9j6X
xVKdraSaEitDneONrSAt2tE/RwRxh2ASxqQfdF88zyDI8/ma608tHc
FROaNsn5hF+/wzjRK9akdhp5WjA5HXhg2OlkwKvSMhGlSgotRj5pr4Ebxjegysy1mEWRFN/vh/oNq4uHQy8adpfogaVELkI/Z2nuAdQk
+uMy6D1hrKhUWubmBPxTbG00IWF25Tyuz8hnFRP9+gB/P
NRlF59/EHy27a72nirvuOyfxKnx/Mn+FD9Ah59OSLhWuo3sN9Im8yc2cliecwMz+DmTtE7TwzNw9v2zfxU9JDQwyLtppULiGpmKFOLHjz
+SVGxSbVsWS//IyNK1GrQ== gschoenb@gschoenb-X220""",
"""ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU
GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3
Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA
t3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/En
mZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx
NrRFi9wrf+M7Q== schacon@mylaptop.local"""]
Validator_invalid_ssh: list = ["lol.", "ssh-rsa lol."]
#Y-%m-%d %H:%M:%S
Validator_valid_datetime: list = ["2020-10-07 14:15:30", "2020-01-07 12:23:11"]
Validator_ivvalid_datetime: list = ["20201007 14:15:30", "2020-01-07 122311"]
Validator_valid_checkname_names: list = ["HalloWelt"]
Validator_invalid_checkname_names: list = ["\\n", "\n\b"]

@ -0,0 +1,30 @@
"id","username","email","name","pubkey","timestamp","status"
1,"darksider1","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 16:12:52",1
1,"darksider2","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 12:11:52",1
1,"darksider3","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 13:14:52",0
1,"darksider4","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 14:15:52",1
1,"darksider5","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 15:19:52",1
1,"darksider6","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 16:12:52",1
1,"darksider7","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 17:11:52",0
1,"darksider8","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 18:14:52",1
1,"darksider9","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 19:15:52",1
1,"darksider10","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 20:19:52",1
1,"darksider11","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 21:12:52",0
1,"darksider12","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 22:11:52",1
1,"darksider13","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 23:14:52",0
1,"darksider14","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 00:15:52",1
1,"darksider15","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 01:19:52",0
1,"darksider16","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 03:15:52",1
1,"darksider17","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 02:19:52",1
1,"darksider18","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 05:12:52",0
1,"darksider19","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 04:11:52",1
1,"darksider20","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 06:14:52",0
1,"darksider21","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 07:15:52",1
1,"darksider22","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 09:19:52",0
1,"darksider23","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 08:15:52",1
1,"darksider24","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 10:19:52",1
1,"darksider25","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 11:12:52",0
1,"darksider26","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 12:11:52",1
1,"darksider27","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 13:14:52",0
1,"darksider28","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 14:15:52",1
1,"darksider29","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 15:19:52",0
1 id username email name pubkey timestamp status
2 1 darksider1 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 16:12:52 1
3 1 darksider2 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 12:11:52 1
4 1 darksider3 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 13:14:52 0
5 1 darksider4 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 14:15:52 1
6 1 darksider5 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 15:19:52 1
7 1 darksider6 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 16:12:52 1
8 1 darksider7 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 17:11:52 0
9 1 darksider8 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 18:14:52 1
10 1 darksider9 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 19:15:52 1
11 1 darksider10 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 20:19:52 1
12 1 darksider11 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 21:12:52 0
13 1 darksider12 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 22:11:52 1
14 1 darksider13 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 23:14:52 0
15 1 darksider14 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 00:15:52 1
16 1 darksider15 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 01:19:52 0
17 1 darksider16 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 03:15:52 1
18 1 darksider17 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 02:19:52 1
19 1 darksider18 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 05:12:52 0
20 1 darksider19 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 04:11:52 1
21 1 darksider20 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 06:14:52 0
22 1 darksider21 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 07:15:52 1
23 1 darksider22 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 09:19:52 0
24 1 darksider23 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 08:15:52 1
25 1 darksider24 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 10:19:52 1
26 1 darksider25 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 11:12:52 0
27 1 darksider26 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 12:11:52 1
28 1 darksider27 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 13:14:52 0
29 1 darksider28 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 14:15:52 1
30 1 darksider29 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 15:19:52 0

@ -0,0 +1,14 @@
"id","username","email","name","pubkey","timestamp","status"
1,"darksider3","tester@testentry.de","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro","2019-10-12 11:19:52",6
2,"Darksider2","notapproved@tester.com","Unapproved Tester","Unknown","2019-10-12 11:20:34",6
3,"darksider4","penis@cocksucker.email","Luca","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro",12345,1
4,"darksider5","pissfotze@example.com","empty","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOf5ZbbXbTyVD1WN0XES3lPG9jXGoYxcKhXPBq5D3ZwG test@example-com","1970-00-00",0
7,"3kliksphilip","test@test",,"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIjb0JardY8xeB5YwIPn9Z0HSEPfy3wEIB90f/rEgy33 test@example.com","11.10.20 20:20",1
8,"root","mailtest@tester.testets.test","Tester sagtHallo","ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBiR1xpeeBuKALRi1czgVbyPbGW7Y6It72PRULFDhiFM drip@kvn.home.steinke.pro","10.12.23 13:37",0
6213663,"4kliksphilip",,"Testtest","test@example.com nokey","12.10.19 01:15",1
11,"name mitleerzeichen","test","Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","Unknown","1970-00-00",-7
8,"$HOME","wurst",,"invalid",1,3
12,"loremipsumdolorsitametconsetetursadipisceoraetorbi","eierkopf",,";;--",2,4
16,"darksider3","penis@darksider3.de",,,1970,200
-1,"darksider17","haha@lol.nigge.rs",,,"penis",6
1 id username email name pubkey timestamp status
2 1 darksider3 tester@testentry.de Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 2019-10-12 11:19:52 6
3 2 Darksider2 notapproved@tester.com Unapproved Tester Unknown 2019-10-12 11:20:34 6
4 3 darksider4 penis@cocksucker.email Luca ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDFsOkf8NTRY++69dCMO8hJslNCWIJMhgHxwN4vSPUG2 drip@kvn.home.steinke.pro 12345 1
5 4 darksider5 pissfotze@example.com empty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOf5ZbbXbTyVD1WN0XES3lPG9jXGoYxcKhXPBq5D3ZwG test@example-com 1970-00-00 0
6 7 3kliksphilip test@test ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIjb0JardY8xeB5YwIPn9Z0HSEPfy3wEIB90f/rEgy33 test@example.com 11.10.20 20:20 1
7 8 root mailtest@tester.testets.test Tester sagtHallo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBiR1xpeeBuKALRi1czgVbyPbGW7Y6It72PRULFDhiFM drip@kvn.home.steinke.pro 10.12.23 13:37 0
8 6213663 4kliksphilip Testtest test@example.com nokey 12.10.19 01:15 1
9 11 name mitleerzeichen test Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. Unknown 1970-00-00 -7
10 8 $HOME wurst invalid 1 3
11 12 loremipsumdolorsitametconsetetursadipisceoraetorbi eierkopf ;;-- 2 4
12 16 darksider3 penis@darksider3.de 1970 200
13 -1 darksider17 haha@lol.nigge.rs penis 6
Loading…
Cancel
Save