SQLite erste Tests (Ticketsystem)

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Hey,

ich habe mal versucht eine Art "Ticketsystem" mit SQLite zu bauen, es ist erstmal nur ein grober Test (komplettes Exceptionhandling fehlt) um in SQLite rein zu kommen und auch mein erstes Projekt in die Richtung Datenbanken.
Ich würde mich freuen wenn mal jemand drüber schauen würde und ob ich das soweit alles richtig gemacht habe oder ob es eventuell Möglichkeiten gibt da was zu vereinfachen bzw. zu verbessern.

Code: Alles auswählen

import getpass
import hashlib
import sqlite3
import pathlib

DATABASE_PATH = pathlib.Path(r"C:\Users\xxx\Desktop\TEST.db")

def create_tables(connection):    
    cursor = connection.cursor()
    sql = "CREATE TABLE IF NOT EXISTS User(customer_number INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT, password TEXT NOT NULL)" 
    cursor.execute(sql)
    sql = "CREATE TABLE Tickets(ticket_number INTEGER PRIMARY KEY AUTOINCREMENT, author_number INTEGER NOT NULL, topic TEXT NOT NULL, content TEXT NOT NULL)"
    cursor.execute(sql)
    connection.commit()
    print("Datenbank angelegt!")
   
def hash_password(password):
    return hashlib.sha256(password.encode()).hexdigest()
    
def is_valild_user_registration(connection, username, passwort):
    cursor = connection.cursor()
    sql = "SELECT user_name FROM User"
    all_user = [x[0] for x in cursor.execute(sql).fetchall()]   
    return True if username not in all_user else False
 
def register(connection):
    cursor = connection.cursor()
    new_user_username = input("Bitte Namen eingeben: ")
    new_user_passwort = hash_password(getpass.getpass("Bitte Kennwort eingeben: "))
    if is_valild_user_registration(connection, new_user_username, new_user_passwort):
        sql = "INSERT INTO User (user_name, password) VALUES (?,?)"
        cursor.execute(sql, (new_user_username, new_user_passwort))
        connection.commit()
        print("\nAccount angelegt!")
    else:
        print("\nDieser Anmeldename wird schon benutzt.")
       
def login(connection):
    login_name = input("Bitte Namen eingeben: ")
    login_passwort = hash_password(getpass.getpass("Bitte Kennwort eingeben: "))
    cursor = connection.cursor()
    sql = "SELECT customer_number FROM User WHERE user_name = ? AND password = ?"
    customer_number = cursor.execute(sql, (login_name, login_passwort)).fetchone()
    if customer_number:
        print(f"\nWillkommen {login_name}")
        return customer_number[0]      
    else:
        print("\nFalsche Anmeldedaten!")
        

def create_ticket(connection, customer_number):
    cursor = connection.cursor()
    topic = input("Bitte gib das Thema an: ")
    content = input("Bitte gib eine genauere Beschreibung an: ")
    sql = "INSERT INTO Tickets (author_number, topic, content) VALUES (?,?,?)"
    cursor.execute(sql,(customer_number, topic, content))
    connection.commit()
    print(f"Das Ticket [{topic}] wurde erstellt!")

def show_tickets(connection, customer_number):
    cursor = connection.cursor()
    sql = "SELECT ticket_number, topic, content FROM Tickets WHERE author_number = ?"
    tickets = cursor.execute(sql, (customer_number,)).fetchall()
    if tickets:
        for ticket in tickets:
            ticket_number, topic, content = ticket
            print(f"""\n[Ticketnummer]: {ticket_number}
[Thema]: 
{topic}
            
[Inhalt]:
{content}\n\n""")


def delete_account(connection, customer_number):
    cursor = connection.cursor()
    sql = "DELETE FROM User WHERE customer_number = ?"
    cursor.execute(sql,(customer_number,))
    sql = "DELETE FROM Tickets WHERE author_number = ?"
    cursor.execute(sql,(customer_number,))
    connection.commit()
    print("\nAccount wurde gelöscht!")

    
def main():
    if not DATABASE_PATH.is_file():
        with sqlite3.connect(DATABASE_PATH) as connection:
            create_tables(connection)
    while True:
        customer_number = None
        while customer_number == None:
            menue_choice = int(input("""\nWas möchtest du tun: 

1 - Registrieren
2 - Anmelden
3 - Beenden

> """))
            if menue_choice == 1:
                with sqlite3.connect(DATABASE_PATH) as connection:
                    register(connection)
            elif menue_choice == 2:
                with sqlite3.connect(DATABASE_PATH) as connection:
                    customer_number = login(connection)
            elif menue_choice == 3:
                break
        while customer_number:
            menue_choice =  int(input("""\nWas möchtest du tun:

1 - Ticket erstellen
2 - Meine Tickets ansehen
3 - Abmelden
4 - Account löschen

> """))
            if menue_choice == 1:
                with sqlite3.connect(DATABASE_PATH) as connection:
                    create_ticket(connection, customer_number)
            elif menue_choice == 2:
                with sqlite3.connect(DATABASE_PATH) as connection:
                    show_tickets(connection, customer_number)
            elif menue_choice == 3:
                customer_number = None
            elif menue_choice == 4:
                delete_account(connection, customer_number)
                customer_number = None
        break
        
main()
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Du weißt, dass Du ein völlig unsicheres System benutzt, Passwörter abzulegen? So könntest Du die Passwörter auch gleich im Klartext speichern. Lies hier: https://hynek.me/articles/storing-passwords/, wie man Argon2 als sichere Methode nutzt.

`is_valild_user_registration` sucht einfach nur nach dem Nutzernamen. Das sollte man die Datenbank machen lassen und nicht in Python nachprogrammieren.
Das ist aber in `register` auch unsicher programmiert. In der Zeit, in der Du fragst, ob der Nutzer existiert und der Zeit, in der Du ihn tatsächlich anlegst, könnte schon jemand anderes einen Nutzer mit diesem Namen anlegen. Deshalb mußt Du den Nutzernamen als einzigartig (unique) in der Datenbank definieren und einfach per INSERT versuchen, ob das klappt. Falls nicht, existiert er schon.
In `login` sollte nicht einmal explizit ein Wert und einmal implizit None zurückgegeben werden. Entweder explizit None, oder besser eine Exception werfen.
In `show_tickets` ist das `if tickets` unnötig, da eine for-Schleife bei einer leeren Liste exakt 0mal durchlaufen wird.

In `main`: die while-Schleifen sind eigentlich while-True-Schleifen, die an der passenden Stelle per break verlassen werden sollten.
Eine while-Schleife, die immer am Ende des ersten Durchgangs per break beendet wird, ist keine while-Schleife.
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Hey,

vielen Dank für die Rückmeldung :)

Ich habe jetzt soweit alles umgesetzt bis auf folgendes, da wusste ich nicht wie das gemeint ist:
Sirius3 hat geschrieben: Freitag 15. Mai 2020, 14:44 In `login` sollte nicht einmal explizit ein Wert und einmal implizit None zurückgegeben werden. Entweder explizit None, oder besser eine Exception werfen.
mein Code jetzt:

Code: Alles auswählen

import getpass
import sqlite3
import pathlib
import argon2

DATABASE_PATH = pathlib.Path(r"C:\Users\xxx\Desktop\TEST.db")

def create_tables(connection):    
    cursor = connection.cursor()
    sql = "CREATE TABLE IF NOT EXISTS User(customer_number INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT UNIQUE, password TEXT NOT NULL)" 
    cursor.execute(sql)
    sql = "CREATE TABLE Tickets(ticket_number INTEGER PRIMARY KEY AUTOINCREMENT, author_number INTEGER NOT NULL, topic TEXT NOT NULL, content TEXT NOT NULL)"
    cursor.execute(sql)
    connection.commit()
    print("Datenbank angelegt!")
   
    
def register(connection):
    cursor = connection.cursor()
    password_hasher = argon2.PasswordHasher()
    new_user_username = input("Bitte Namen eingeben: ")
    new_user_passwort = password_hasher.hash(getpass.getpass("Bitte Kennwort eingeben: "))
    try:
        sql = "INSERT INTO User (user_name, password) VALUES (?,?)"
        cursor.execute(sql, (new_user_username, new_user_passwort))
        connection.commit()
        print("Account angelegt!")
    except sqlite3.IntegrityError:
        print("Dieser Anmeldename wird schon benutzt.")
     
       
def login(connection):
    password_hasher = argon2.PasswordHasher()
    cursor = connection.cursor()
    login_name = input("Bitte Namen eingeben: ")
    login_passwort = getpass.getpass("Bitte Kennwort eingeben: ")
    sql = "SELECT password FROM User WHERE user_name = ?"
    user_password = cursor.execute(sql, (login_name,)).fetchone()
    if user_password:
        try:
            if password_hasher.verify(user_password[0], login_passwort):
                sql = "SELECT customer_number FROM User WHERE user_name = ?"
                customer_number = cursor.execute(sql, (login_name,)).fetchone()[0]
                return customer_number               
        except argon2.exceptions.VerificationError:
            print("Falsche Anmeldedaten")
    else:
        print("Falsche Anmeldedaten")
            

        
def create_ticket(connection, customer_number):
    cursor = connection.cursor()
    topic = input("Bitte gib das Thema an: ")
    content = input("Bitte gib eine genauere Beschreibung an: ")
    sql = "INSERT INTO Tickets (author_number, topic, content) VALUES (?,?,?)"
    cursor.execute(sql,(customer_number, topic, content))
    connection.commit()
    print(f"Das Ticket [{topic}] wurde erstellt!")

def show_tickets(connection, customer_number):
    cursor = connection.cursor()
    sql = "SELECT ticket_number, topic, content FROM Tickets WHERE author_number = ?"
    tickets = cursor.execute(sql, (customer_number,)).fetchall()
    for ticket in tickets:
        ticket_number, topic, content = ticket
        print(f"""\n[Ticketnummer]: {ticket_number}
[Thema]: 
{topic}
            
[Inhalt]:
{content}\n\n""")


def delete_account(connection, customer_number):
    cursor = connection.cursor()
    sql = "DELETE FROM User WHERE customer_number = ?"
    cursor.execute(sql,(customer_number,))
    sql = "DELETE FROM Tickets WHERE author_number = ?"
    cursor.execute(sql,(customer_number,))
    connection.commit()
    print("\nAccount wurde gelöscht!")

    
def main():
    if not DATABASE_PATH.is_file():
        with sqlite3.connect(DATABASE_PATH) as connection:
            create_tables(connection)
    while True:
        while True:
            menue_choice = int(input("""\nWas möchtest du tun: 

1 - Registrieren
2 - Anmelden


> """))
            if menue_choice == 1:
                with sqlite3.connect(DATABASE_PATH) as connection:
                    register(connection)
            elif menue_choice == 2:
                with sqlite3.connect(DATABASE_PATH) as connection:
                    customer_number = login(connection)
                    if customer_number:
                        break
        while True:
            menue_choice =  int(input("""\nWas möchtest du tun:

1 - Ticket erstellen
2 - Meine Tickets ansehen
3 - Abmelden
4 - Account löschen

> """))
            if menue_choice == 1:
                with sqlite3.connect(DATABASE_PATH) as connection:
                    create_ticket(connection, customer_number)
            elif menue_choice == 2:
                with sqlite3.connect(DATABASE_PATH) as connection:
                    show_tickets(connection, customer_number)
            elif menue_choice == 3:
                break
            elif menue_choice == 4:
                with sqlite3.connect(DATABASE_PATH) as connection:
                    delete_account(connection, customer_number)
                break
        
main()
Benutzeravatar
sparrow
User
Beiträge: 4164
Registriert: Freitag 17. April 2009, 10:28

@Jankie: login() gibt bei einem erfolgreichen Login etwas zurück, was du im Code auch schreibst (explizit). Wenn das Login aber nicht erfolgreich war, schreibst du aber nichts im Code, was dazu führt dass None zurückgegeben wird (implizit, weil es nicht im Code steht).
Es ist viel einfacher zu lesen, wenn jede Art der Rückgabe auch im Code steht. Daher der Hinweis von Sirius3 entweder explizit auch None zurückzugeben - oder besser gleich eine Exception zu werfen.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Jankie: Anmerkungen:

Der `main()`-Aufruf ist nicht durch ``if __name__ …`` ”geschützt”. Man kann das Modul also nicht importieren ohne dass das Programm los läuft.

Für meinen Geschmack werden da zu oft Verbindungen im Code hergestellt. Das würde ich genau *einmal* machen.

Das erstellen der Tabellen von der Existenz der Datenbankdatei abhängig zu machen würde ich als Fehler ansehen, weil die Datei ja durchaus existieren kann, aber auch ohne die Tabellen.

Tabellennamen sind üblicherweise in der Einzahl benannt, weil so eine CREATE-Anweisung *einen* Datensatz beschreibt.

Das was hier in der Datenbank `*_number` heisst, würde man eher `*_id` nennen. Beim Primärschlüssel auch wirklich nur `id` ohne was dazu, denn welche `id` das ist, legt ja schon der Tabellenname fest.

Auch wenn SQLite3 (momentan) Fremdschlüssel-Constraints erst berücksichtigt, wenn man das explizit per PRAGMA aktiviert, würde ich die trotzdem deklarieren, weil es auch als Dokumentation für den Leser dient, auch wenn die DB-Software das nicht berücksichtigt.

Das `user_password` in `login()` ein Tupel (oder ähnliches Objekt) mit einem Wert enthält (wenn es den Benutzernamen gibt) ist verwirrend.

Neben dem expliziten `None` muss auch beim Aufruf etwas geändert werden, weil 0 eine gültige Benutzernummer sein kann. Also darf man nicht einfach auf ``if customer_number:`` testen, sondern muss das dort explizit gegen `None` machen.

Das Entpacken von Tupeln in einer Schleife kann man auch gleich im Schleifenkopf erledigen.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import getpass
import pathlib
import sqlite3

import argon2

DATABASE_PATH = pathlib.Path(r"C:\Users\xxx\Desktop\TEST.db")


def create_tables(connection):
    cursor = connection.cursor()
    cursor.execute(
        "CREATE TABLE IF NOT EXISTS user ("
        " id INTEGER PRIMARY KEY AUTOINCREMENT,"
        " user_name TEXT NOT NULL UNIQUE,"
        " password TEXT NOT NULL)"
    )
    cursor.execute(
        "CREATE TABLE IF NOT EXISTS ticket ("
        " id INTEGER PRIMARY KEY AUTOINCREMENT,"
        " author_id INTEGER NOT NULL REFERENCES user(id),"
        " topic TEXT NOT NULL,"
        " content TEXT NOT NULL)"
    )
    connection.commit()
    print("Datenbank angelegt!")


def register(connection):
    new_user_username = input("Bitte Namen eingeben: ")
    new_user_password = argon2.PasswordHasher().hash(
        getpass.getpass("Bitte Kennwort eingeben: ")
    )
    cursor = connection.cursor()
    try:
        cursor.execute(
            "INSERT INTO User (user_name, password) VALUES (?,?)",
            (new_user_username, new_user_password),
        )
        connection.commit()
        print("Account angelegt!")
    except sqlite3.IntegrityError:
        print("Dieser Anmeldename wird schon benutzt.")


def login(connection):
    login_name = input("Bitte Namen eingeben: ")
    login_password = getpass.getpass("Bitte Kennwort eingeben: ")
    cursor = connection.cursor()
    row = cursor.execute(
        "SELECT password FROM user WHERE user_name = ?", (login_name,)
    ).fetchone()
    if row:
        hashed_password = row[0]
        try:
            if argon2.PasswordHasher().verify(hashed_password, login_password):
                return cursor.execute(
                    "SELECT id FROM user WHERE user_name = ?", (login_name,)
                ).fetchone()[0]
        except argon2.exceptions.VerificationError:
            pass

    print("Falsche Anmeldedaten")
    return None


def create_ticket(connection, user_id):
    topic = input("Bitte gib das Thema an: ")
    content = input("Bitte gib eine genauere Beschreibung an: ")
    connection.cursor().execute(
        "INSERT INTO ticket (author_id, topic, content) VALUES (?,?,?)",
        (user_id, topic, content),
    )
    connection.commit()
    print(f"Das Ticket [{topic}] wurde erstellt!")


def show_tickets(connection, user_id):
    tickets = (
        connection.cursor()
        .execute(
            "SELECT id, topic, content FROM ticket WHERE author_id = ?",
            (user_id,),
        )
        .fetchall()
    )
    for ticket_id, topic, content in tickets:
        print(
            f"""\n[Ticketnummer]: {ticket_id}
[Thema]: 
{topic}
            
[Inhalt]:
{content}\n\n"""
        )


def delete_account(connection, user_id):
    cursor = connection.cursor()
    for sql in [
        "DELETE FROM ticket WHERE author_id = ?",
        "DELETE FROM user WHERE id = ?",
    ]:
        cursor.execute(sql, (user_id,))
    connection.commit()
    print("\nAccount wurde gelöscht!")


def main():
    with sqlite3.connect(DATABASE_PATH) as connection:
        create_tables(connection)
        while True:
            while True:
                menue_choice = int(
                    input(
                        """\nWas möchtest du tun: 

1 - Registrieren
2 - Anmelden


> """
                    )
                )
                if menue_choice == 1:
                    register(connection)
                elif menue_choice == 2:
                    user_id = login(connection)
                    if user_id is None:
                        break

            while True:
                menue_choice = int(
                    input(
                        """\nWas möchtest du tun:

1 - Ticket erstellen
2 - Meine Tickets ansehen
3 - Abmelden
4 - Account löschen

> """
                    )
                )
                if menue_choice == 1:
                    create_ticket(connection, user_id)
                elif menue_choice == 2:
                    show_tickets(connection, user_id)
                elif menue_choice == 3:
                    break
                elif menue_choice == 4:
                    delete_account(connection, user_id)
                    break


if __name__ == "__main__":
    main()
Programmlogik und Benutzerinteraktion sind nicht getrennt.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Vielen Dank für die Ausführliche Rückmeldung, ich habe jetzt soweit alles angepasst.
@__blackjack__: der Code von dir war ungetestet fast ganz fehlerlos, es hat nur ein "not" bei if user_id is None gefehlt :)
Ich habe nur folgenden Satz nicht richtig verstanden:

Auch wenn SQLite3 (momentan) Fremdschlüssel-Constraints erst berücksichtigt, wenn man das explizit per PRAGMA aktiviert, würde ich die trotzdem deklarieren, weil es auch als Dokumentation für den Leser dient, auch wenn die DB-Software das nicht berücksichtigt.

Auch beim trennen von Programmlogik und Benutzerinteraktion hab ich noch meine Probleme. Wie genau schaut so eine Trennung denn aus bzw. was sollte ich machen und was nicht?
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Jankie: `ticket.author_id` bezieht sich ja auf die `user.id`. Das bezeichnet man als Fremdschlüssel. SQL hat da Syntax für beim Deklarieren und normalerweise wird das auch von Datenbanken berücksichtigt. Also geprüft ob es die ID, die für `ticket.author_id` beim anlegen oder ändern eines `ticket`-Datensatzes eingesetzt wurde überhaupt gibt. Die Deklaration als Fremdschlüssel ist das zusätzliche ``REFERENCES user(id)`` bei `ticket.author_id`.

Und man könnte auch sagen was beim Löschen oder Ändern passieren soll. Beispielsweise könnte man auch noch ein ON DELETE CASCADE hinzufügen. Dann würde es reichen einen Benutzer zu löschen um automatisch alle mit dem Benutzer verbundenen Tickets zu löschen. Das muss man bei SQLite3 aber erst mit einem ``PRAGMA foreign_keys = ON`` aktivieren. Ohne das versteht SQLite3 zwar die Syntax beim deklarieren, macht aber nichts weiter mit der Information. Aber wie gesagt, hilft es auch dann immer noch dem Leser die Beziehungen zwischen den Tabellen besser zu verstehen. Und ggf auch anderer Software, wenn man die DB/Tabellen beispielsweise mit irgendwelchen Werkzeugen verarbeiten will. Beispielsweise welche die Diagramme erzeugen oder auch SQLAlchemy das vorhandene DBs analysieren kann.

Benutzerinteraktion sind momentan `print()` und `input()`. Das steht überall mit in den Funktionen die Aufgaben erledigen. Also beispielsweise bei der Anmeldung. Wenn man da jetzt eine GUI für schreiben würde, oder eine Webanwendung, dann gibt es jetzt in `login()` Code, der bei allen drei Varianten (Text-UI, GUI, Webanwendung) gleich wäre, und Code der momentan das ganze für GUI und Webanwendung zum Wiederverwenden unbrauchbar macht.

Selbst wenn man nicht vor hat eine andere Benutzerschnittstelle als Textein- und ausgabe zur Verfügung zu stellen: Man könnte so auch nicht, oder nur unnötig schwer, automatisierte Tests schreiben, die beispielsweise mal alle neun Kombinationen aus leerem, gültigen, und ungültigen Benutzernamen und Passwort durchprobieren, ob da jeweils das gewünschte Ergebnis heraus kommt.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Zu der Trennung von Benutzereingaben/Benutzerausgaben und der Logik:

Ich habe ja z.B. folgende Funktion:

Code: Alles auswählen

def register(connection):
    new_user_username = input("Bitte Namen eingeben: ")
    new_user_password = argon2.PasswordHasher().hash(getpass.getpass("Bitte Kennwort eingeben: "))
    cursor = connection.cursor()
    try:
        cursor.execute("INSERT INTO user (user_name, password, admin) VALUES (?,?,?)",(new_user_username, new_user_password, False),)
        connection.commit()
        print("\nAccount angelegt!")
    except sqlite3.IntegrityError:
        print("\nDieser Anmeldename wird schon benutzt.")
um die Logik zu trennen, muss ich die Funktion dann so abändern:

Code: Alles auswählen

def register(connection, new_user_name, new_user_password):
    cursor = connection.cursor()
    try:
        cursor.execute("INSERT INTO user (user_name, password, admin) VALUES (?,?,?)",(new_user_username, new_user_password, False),)
        connection.commit()
        return True
    except sqlite3.IntegrityError:
        return False
und in der main Funktion dann so?:

Code: Alles auswählen

def main():
    new_user_username = input("Bitte Namen eingeben: ")
    new_user_password = argon2.PasswordHasher().hash(getpass.getpass("Bitte Kennwort eingeben: "))
    if register(connection, new_user_name, new_user_password):
        print("Account angelegt!")
    else:
        print("Dieser Anmeldename wird schon benutzt")
Also funktionieren tut es, aber ist das auch der übliche Weg wie man dies macht?

Dann habe ich noch zwei Fragen. Ich möchte quasi noch eine Spalte admin hinzufügen in welche bool Werte kommen, also einfach ob der User Admin ist oder nicht. User mit admin = True sollen alle Tickets lesen können und andere User zu Admins machen, mehr erstmal nicht. Jetzt frag ich mich wie man so einen Admin Account erstellt. Meine Überlegung war, dass beim ersten Ausführen des Codes automatisch ein User Namens Admin erstellt wird (sofern noch nicht vorhanden). Kann man das so machen oder wie würdet Ihr das umsetzen?

Code: Alles auswählen

def create_admin_account(connection):
    cursor = connection.cursor()
    try:
        user_name = "Admin"
        user_password = ""
        cursor.execute("INSERT INTO user (user_name, password, admin) VALUES (?,?,?)",(user_name, user_password, True),)
        new_user_password = argon2.PasswordHasher().hash(getpass.getpass("Bitte Kennwort für Admin eingeben: "))
        cursor.execute("UPDATE user SET password = ? WHERE user_name = ?", (new_user_password, user_name),)
        connection.commit()
    except sqlite3.IntegrityError:
        pass
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Zeile mit PasswordHasher sollte auch noch in `register`.
Zu `create_admin_account`: Du hast doch schon eine Funktion `register` die fast das gleiche macht. Also nutze die auch.
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Und wie kann ich dafür sorgen dass sich sonst keiner als Admin registriert? Also ich kann das ja schlecht als Parameter bei register mit geben bzw. den User selbst aussuchen lassen ob er Admin ist oder nicht, oder?



Hab jetzt mal die ersten Änderungen gemacht also Trennung von Logik und Ein/Ausgaben, ersten Admin Account erstellen (muss ich dann noch anpassen wenn ich weiß wie es richtig geht bzw. wie ich das über die register Funktion nutzen kann, weil habe dort auch noch eine Benutzereingabe drin) und Abfrage ob der User Admin ist, bei der Abfrage ob der User Admin ist ha ich das Gefühl dass ich dort wieder gemurkst habe, weiß aber nicht wie ich es hätte anders machen können.

Oder soll ich einen User mit Name Admin und Passwort Admin von Anfang an vorgeben und dann eine Änderung des Passworts möglich machen?

Code: Alles auswählen

import getpass
import pathlib
import sqlite3
import argon2

DATABASE_PATH = pathlib.Path(r"C:\Users\xxx\Desktop\TEST.db")


def create_tables(connection):
    cursor = connection.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, user_name TEXT NOT NULL UNIQUE, password TEXT NOT NULL, admin BOOLEAN NOT NULL)")
    cursor.execute("CREATE TABLE IF NOT EXISTS ticket (id INTEGER PRIMARY KEY AUTOINCREMENT, author_id INTEGER NOT NULL REFERENCES user(id), topic TEXT NOT NULL, content TEXT NOT NULL)")
    connection.commit()
    return True


def register(connection, new_user_name, new_user_password):
    new_user_password = argon2.PasswordHasher().hash(new_user_password)
    cursor = connection.cursor()
    try:
        cursor.execute("INSERT INTO user (user_name, password, admin) VALUES (?,?,?)",(new_user_name, new_user_password, False),)
        connection.commit()
        return True
    except sqlite3.IntegrityError:
        return False


def login(connection, login_name, login_password):
    cursor = connection.cursor()
    row = cursor.execute("SELECT password FROM user WHERE user_name = ?", (login_name,)).fetchone()
    if row:
        hashed_password = row[0]
        try:
            if argon2.PasswordHasher().verify(hashed_password, login_password):
                return cursor.execute("SELECT id FROM user WHERE user_name = ?", (login_name,)).fetchone()[0]
        except argon2.exceptions.VerificationError:
            pass
    return None


def create_ticket(connection, user_id, topic, content):
    connection.cursor().execute("INSERT INTO ticket (author_id, topic, content) VALUES (?,?,?)",(user_id, topic, content))
    connection.commit()
    return True

def create_admin_account(connection):
    cursor = connection.cursor()
    try:
        user_name = "Admin"
        user_password = ""
        cursor.execute("INSERT INTO user (user_name, password, admin) VALUES (?,?,?)",(user_name, user_password, True),)
        new_user_password = argon2.PasswordHasher().hash(getpass.getpass("Bitte Kennwort für Admin eingeben: "))
        cursor.execute("UPDATE user SET password = ? WHERE user_name = ?", (new_user_password, user_name),)
        connection.commit()
    except sqlite3.IntegrityError:
        pass
    
def is_admin(connection, user_id):
    try:
        return connection.cursor().execute("SELECT admin FROM user WHERE id = ?", (user_id,)).fetchone()[0]
    except TypeError:
        #wenn der User nicht existiert wird ein TypeError: 'NoneType' object is not subscriptable ausgeben, da das obere SQL nichts findet.
        #wenn der User nicht existert kann er kein Admin sein.
        return False

def show_tickets(connection, user_id):
    return connection.cursor().execute("SELECT id, topic, content FROM ticket WHERE author_id = ?",(user_id,),).fetchall()



def delete_account(connection, user_id):
    cursor = connection.cursor()
    for sql in ["DELETE FROM ticket WHERE author_id = ?","DELETE FROM user WHERE id = ?"]:
        cursor.execute(sql, (user_id,))
    connection.commit()
    return True


def main():
    with sqlite3.connect(DATABASE_PATH) as connection:
        if create_tables(connection):
            print("Datanbank angelegt!")
            create_admin_account(connection)
        while True:
            while True:
                menue_choice = int(input("""\nWas möchtest du tun: 

1 - Registrieren
2 - Anmelden

> """))
                if menue_choice == 1:
                    new_user_name = input("Bitte Namen eingeben: ")
                    new_user_password = getpass.getpass("Bitte Kennwort eingeben: "))
                    if register(connection, new_user_name, new_user_password):
                        print("Account angelegt")
                    else:
                        print("Dieser Anmeldename wird bereits genutzt")
                elif menue_choice == 2:
                    login_name = input("Bitte Namen eingeben: ")
                    login_password = getpass.getpass("Bitte Kennwort eingeben: ")
                    user_id = login(connection, login_name, login_password)
                    if is_admin(connection,user_id):
                        print("Als Admin eingeloggt!")
                    if user_id is not None:
                        break
                    else:
                        print("Falsche Anmeldedaten")
            while True:
                menue_choice = int(input("""\nWas möchtest du tun:

1 - Ticket erstellen
2 - Meine Tickets ansehen
3 - Abmelden
4 - Account löschen

> """))
                if menue_choice == 1:
                    topic = input("Bitte gib das Thema an: ")
                    content = input("Bitte gib eine genauere Beschreibung an: ")
                    if create_ticket(connection, user_id, topic, content):
                        print(f"Das Ticket [{topic}] wurde erstellt!")
                    else:
                        print("Fehler beim Ticket erstellen.")
                elif menue_choice == 2:
                    tickets = show_tickets(connection, user_id)
                    for ticket_id, topic, content in tickets:
                        print(f"""\n[Ticketnummer]: 
{ticket_id}\n
[Thema]: 
{topic}\n     
[Inhalt]:
{content}\n\n""")
                    else:
                        print("Keine Tickets!")
                elif menue_choice == 3:
                    break
                elif menue_choice == 4:
                    if delete_account(connection, user_id):
                        print("\nAccount wurde gelöscht!")
                    break


if __name__ == "__main__":
    main()
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Natürlich kannst Du nicht verhinder, dass sich jeder als Admin anmeldet, wenn er direkten Zugriff auf die Datenbank hat.

Im Programm ist es aber der selbe Code:

Code: Alles auswählen

def register(connection, new_user_name, new_user_password, admin=False):
    new_user_password = argon2.PasswordHasher().hash(new_user_password)
    cursor = connection.cursor()
    try:
        cursor.execute("INSERT INTO user (user_name, password, admin) VALUES (?,?,?)",(new_user_name, new_user_password, admin),)
        connection.commit()
        return True
    except sqlite3.IntegrityError:
        return False

def create_admin_account(connection, admin_password):
    register(connection, "Admin", admin_password, admin=True)
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Ahhhh, daran einen Parameter per default auf False zu setzen hab ich gar nicht gedacht, danke! :) Allerdings soll der Admin Account ja nur erstellt werden, wenn keiner vorhanden ist. bzw. die Eingabe des Passworts soll nur erfolgen wenn der Admin Account noch nicht da ist. Da brauch ich dann noch eine Abfrage ob der Admin schon existiert, macht man das besser indem man sich per SQL eine Liste aller User holt und schaut ob der Name drin ist oder mit SELECT * FROM user WHERE user_name = 'Admin' und wenn das ein Error erzeugt den dann Abfangen und dann erst den Admin hinzufügen? Ich hoffe du versteht mein Babbel einigermaßen.
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Okay ich glaube ich habe die richtige Lösung gefunden

Code: Alles auswählen

def admin_exist(connection):
    return connection.cursor().execute("SELECT EXISTS(SELECT 1 FROM user WHERE user_name = ?)", ("Admin",)).fetchone()[0]
und dann in der main():

Code: Alles auswählen

def main():
    with sqlite3.connect(DATABASE_PATH) as connection:
        if create_tables(connection):
            if not admin_exist(connection):
                admin_password = getpass.getpass("Bitte Kennwort für Admin eingeben: ")
                create_admin_account(connection, admin_password)
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Jankie: Dann muss der Admin ja Admin heissen. Das heisst jeder Angreifer wüsste auch das er mindestens einen Benutzernamen in dem System kennt den er nicht mehr raten oder anderweitig in Erfahrung bringen muss, sondern wo er nur noch das Passwort erraten muss. Ich würde da eher testen ob es mindestens einen Benutzer mit Admin*rechten* gibt.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Danke __blackjack__

Ich habe es jetzt geändert. Es wird jetzt geprüft ob ein User mit admin = True existiert.

Code: Alles auswählen

def admin_exist(connection):
    return connection.cursor().execute("SELECT EXISTS(SELECT 1 FROM user WHERE admin = ?)", (True,)).fetchone()[0]
Wenn das nicht der Fall ist kann der User einen Benutzernamen und ein Passwort für den Admin Account übermitteln.

Code: Alles auswählen

if not admin_exist(connection):
    admin_name = input("Bitte den Benutzernamen für Admin eingeben: ")
    admin_password = getpass.getpass("Bitte Kennwort für Admin eingeben: ")
    register(connection, admin_name, admin_password, admin=True)

Code: Alles auswählen

def register(connection, new_user_name, new_user_password, admin=False):
    new_user_password = argon2.PasswordHasher().hash(new_user_password)
    try:
        connection.cursor().execute("INSERT INTO user (user_name, password, admin) VALUES (?,?,?)",(new_user_name, new_user_password, admin),)
        connection.commit()
        return True
    except sqlite3.IntegrityError:
        return False
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Habe jetzt noch ergänzt, dass ein Admin einen anderen User über den Namen auch zu einem Admin machen kann und dass ein Admin die Tickets von allen Usern sehen kann.

Code: Alles auswählen

def make_user_to_admin(connection, user_name):
    user_exists = connection.cursor().execute("SELECT EXISTS(SELECT 1 FROM user WHERE user_name = ?)", (user_name,)).fetchone()[0]
    if user_exists:
        connection.cursor().execute("UPDATE user SET admin = ? WHERE user_name = ?",(True, user_name))
        connection.commit()
        return True
    else:
        return False

Code: Alles auswählen

def show_tickets(connection, user_id):
    if is_admin(connection, user_id):
        return connection.cursor().execute("SELECT id, author_id, topic, content FROM ticket").fetchall()
    else:
        return connection.cursor().execute("SELECT id, author_id, topic, content FROM ticket WHERE author_id = ?",(user_id,)).fetchall()
Da aber die Funktion dass man jemanden zum Admin machen kann nur für Admins sichtbar sein soll, habe ich Probleme mit der Menuesteuerung. Wie macht man das am besten? Habe es jetzt mal so versucht:


Wird im main() aufgerufen:

Code: Alles auswählen

def print_menue(connection, user_id):
    while True:
        print("\nWas möchtest du tun:\n")
        print("1 - Ticket erstellen")
        print("2 - Meine Tickets ansehen")
        print("3 - Abmelden")
        print("4 - Account löschen")
        if is_admin(connection, user_id):
            print("5 - Adminrecht an User geben")
        menue_choice = int(input("\n> "))
        if menue_choice in range(1,6):
            return menue_choice
        else:
            print("Falsche Eingabe!\n")
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Tatsache dass execute auch einen Cursor zurückliefert, sollte man nicht verwenden. Das ist nicht Teil der offiziellen DB-Schnittstelle in Python.
Ein vorheriges Abfragen ist auch gar nicht nötig, weil UPDATE einfach nichts macht, wenn der User nicht existiert.

Code: Alles auswählen

def make_user_to_admin(connection, user_name):
    cursor = connection.cursor()
    cursor.execute("UPDATE user SET admin = ? WHERE user_name = ?", (True, user_name))
    return cursor.rowcount != 0
die Existenz kann man auch in Python abfragen:

Code: Alles auswählen

def admin_exist(connection):
    cursor = connection.cursor()
    cursor.execute("SELECT 1 FROM user WHERE admin = ?", (True,))
    return cursor.fetchone() is not None
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Genau weil es ja nichts zurückgibt habe ich die Abfrage da noch rein gemacht. Da sonst der Benutzer keine Rückmeldung bekommt ob sein vorhaben geglückt ist oder nicht. Aber da scheint mir deine Lösung besser zu sein, danke :)

Meinst du mit "Die Tatsache dass execute auch einen Cursor zurückliefert, sollte man nicht verwenden. Das ist nicht Teil der offiziellen DB-Schnittstelle in Python." dass ich vorher connection.cursor() an eine variable binden soll und dann cursor.execute() benutzen soll? Habe den Satz nicht ganz verstanden.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Jankie: `cursor.execute()` gibt ”nichts” zurück. Bei `sqlite3` wird da zwar der Cursor zurückgegeben, das ist aber nicht garantiert, weil das nicht in der DB API V2-Dokumentation steht. Du kannst da also nicht einfach Cursor-Methoden auf dem Ergebnis aufrufen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Also muss ich wenn ich das richtig verstehe statt:

Code: Alles auswählen

def show_tickets(connection, user_id):
    if is_admin(connection, user_id):
        return connection.cursor().execute("SELECT id, author_id, topic, content FROM ticket").fetchall()
    else:
        return connection.cursor().execute("SELECT id, author_id, topic, content FROM ticket WHERE author_id = ?",(user_id,)).fetchall()
besser

Code: Alles auswählen

def show_tickets(connection, user_id):
    cursor = connection.cursor()
    if is_admin(connection, user_id):
        cursor.execute("SELECT id, author_id, topic, content FROM ticket")
        return cursor.fetchall()
    else:
        cursor.execute("SELECT id, author_id, topic, content FROM ticket WHERE author_id = ?",(user_id,))
        return cursor.fetchall()
machen?
Antworten