Primitive API server – not to be used in prod
User model for API auth
This commit is contained in:
parent
4b58d1a474
commit
f5bc8a766e
6 changed files with 201 additions and 2 deletions
|
@ -4,6 +4,7 @@ from pathlib import Path
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from classes.vessel import Vessel
|
from classes.vessel import Vessel
|
||||||
|
from classes.user import User
|
||||||
|
|
||||||
|
|
||||||
class MonsterConfig:
|
class MonsterConfig:
|
||||||
|
@ -28,16 +29,20 @@ class MonsterConfig:
|
||||||
# Read Vessels from the config file
|
# Read Vessels from the config file
|
||||||
if section.startswith("Vessel"):
|
if section.startswith("Vessel"):
|
||||||
self.vessels.append(Vessel.fromConfig(parser[section]))
|
self.vessels.append(Vessel.fromConfig(parser[section]))
|
||||||
|
if section.startswith("User"):
|
||||||
|
self.users.append(User.fromConfig(parser[section]))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pyadonis = Path(parser["MONSTER"]["PyAdonis"])
|
self.pyadonis = Path(parser["MONSTER"]["PyAdonis"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print(f"PyAdonis is not defined in the MONSTER section of {path}, some features may be missing.")
|
print(f"PyAdonis is not defined in the MONSTER section of {path}, some features may be missing.")
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, path: Union[str, Path]) -> None:
|
def __init__(self, path: Union[str, Path]) -> None:
|
||||||
"""Initialize a new (empty) MonsterConfig object
|
"""Initialize a new (empty) MonsterConfig object
|
||||||
"""
|
"""
|
||||||
self.vessels = []
|
self.vessels = []
|
||||||
|
self.users = []
|
||||||
self.pyadonis = None
|
self.pyadonis = None
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
|
|
45
classes/user.py
Normal file
45
classes/user.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from classes.database import Database
|
||||||
|
|
||||||
|
from configparser import SectionProxy
|
||||||
|
from typing import Optional, Union
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from MySQLdb.cursors import DictCursor
|
||||||
|
from bcrypt import hashpw, gensalt
|
||||||
|
|
||||||
|
from const import *
|
||||||
|
|
||||||
|
|
||||||
|
class User:
|
||||||
|
"""Class describing a User
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def fromConfig(cls, config: SectionProxy):
|
||||||
|
"""Create User object from a User section in the Config file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config (configparser.SectionProxy): User section defining a User
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: Raised if section does not contain Password parameter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
classes.user.User: User object for the user specified in
|
||||||
|
the config section
|
||||||
|
"""
|
||||||
|
|
||||||
|
return cls(config.name.split()[1], config["Password"])
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, username: str, password: str) -> None:
|
||||||
|
"""Initialize new Vessel object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Name of the Vessel
|
||||||
|
"""
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
|
||||||
|
def validatePasword(self, password) -> bool:
|
||||||
|
return password == self.password
|
1
const.py
1
const.py
|
@ -13,6 +13,7 @@ QUERY_USER_CREATE = "INSERT INTO mdl_user(username, email, firstname, lastname,
|
||||||
QUERY_ASSIGN_ROLE = "INSERT INTO mdl_role_assignments(roleid, contextid, userid, timemodified) VALUES (%s, %s, %s, %s)"
|
QUERY_ASSIGN_ROLE = "INSERT INTO mdl_role_assignments(roleid, contextid, userid, timemodified) VALUES (%s, %s, %s, %s)"
|
||||||
QUERY_GET_ROLE = "SELECT * FROM mdl_role_assignments WHERE contextid = %s AND userid = %s"
|
QUERY_GET_ROLE = "SELECT * FROM mdl_role_assignments WHERE contextid = %s AND userid = %s"
|
||||||
QUERY_GET_USERID = "SELECT * FROM mdl_user WHERE username = %s"
|
QUERY_GET_USERID = "SELECT * FROM mdl_user WHERE username = %s"
|
||||||
|
QUERY_USER_GET_PASSWORD = "SELECT password FROM mdl_user WHERE id = %s"
|
||||||
|
|
||||||
QUERY_COURSE = "SELECT * FROM mdl_course"
|
QUERY_COURSE = "SELECT * FROM mdl_course"
|
||||||
QUERY_ENROL = "SELECT * FROM mdl_enrol"
|
QUERY_ENROL = "SELECT * FROM mdl_enrol"
|
||||||
|
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mysqlclient
|
||||||
|
paramiko
|
||||||
|
sshtunnel
|
143
server.py
Normal file
143
server.py
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import socketserver
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import pathlib
|
||||||
|
import json
|
||||||
|
|
||||||
|
from classes.config import MonsterConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ReportMonsterHandler(socketserver.BaseRequestHandler):
|
||||||
|
def requires_login(func, *args, **kwargs) :
|
||||||
|
def inner(self, *args, **kwargs) :
|
||||||
|
if not self.user:
|
||||||
|
return "95 Authentication Required: use login to authenticate first"
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
return inner
|
||||||
|
|
||||||
|
def command_login(self, *args):
|
||||||
|
if not len(args) == 2:
|
||||||
|
return "90 Invalid Argument: login takes exactly two arguments"
|
||||||
|
|
||||||
|
for cuser in self.server.config.users:
|
||||||
|
if cuser.username == args[0]:
|
||||||
|
if cuser.validatePasword(args[1]):
|
||||||
|
self.user = cuser
|
||||||
|
return f"10 Login Successful: logged in as {self.user.username}"
|
||||||
|
|
||||||
|
return "99 Authentication Failed: username or password incorrect"
|
||||||
|
|
||||||
|
def command_whoami(self, *args):
|
||||||
|
if args:
|
||||||
|
return "90 Invalid Argument: whoami takes no arguments"
|
||||||
|
if self.user:
|
||||||
|
return "11 Logged In: logged in as {self.user.username}"
|
||||||
|
return "12 Not Logged In"
|
||||||
|
|
||||||
|
@requires_login
|
||||||
|
def command_vessels(self, *args):
|
||||||
|
return "20 OK: " + json.dumps([vessel.name for vessel in self.server.config.vessels])
|
||||||
|
|
||||||
|
@requires_login
|
||||||
|
def command_users(self, *args):
|
||||||
|
if not len(args) == 1:
|
||||||
|
return "90 Invalid Argument: users takes exactly one argument"
|
||||||
|
|
||||||
|
vessels = list(filter(lambda v: v.name == args[0], self.server.config.vessels))
|
||||||
|
|
||||||
|
if not vessels:
|
||||||
|
return f"92 No Such Object: no vessel called {args[0]} found"
|
||||||
|
if len(vessels) > 1:
|
||||||
|
return f"98 Configuration Error: multiple vessels called {args[0]} found"
|
||||||
|
|
||||||
|
vessel = vessels[0]
|
||||||
|
users = vessel.getUsers()
|
||||||
|
|
||||||
|
result = [user for user in users.values()]
|
||||||
|
|
||||||
|
return "20 OK: " + json.dumps(result)
|
||||||
|
|
||||||
|
@requires_login
|
||||||
|
def command_user(self, *args):
|
||||||
|
if not len(args) == 2:
|
||||||
|
return "90 Invalid Argument: user takes exactly two arguments"
|
||||||
|
|
||||||
|
vessels = list(filter(lambda v: v.name == args[0], self.server.config.vessels))
|
||||||
|
|
||||||
|
if not vessels:
|
||||||
|
return f"92 No Such Object: no vessel called {args[0]} found"
|
||||||
|
if len(vessels) > 1:
|
||||||
|
return f"98 Configuration Error: multiple vessels called {args[0]} found"
|
||||||
|
|
||||||
|
vessel = vessels[0]
|
||||||
|
users = vessel.getUsers(username = args[1])
|
||||||
|
|
||||||
|
if not users:
|
||||||
|
return f"92 No Such Object: no user called {args[1]} found on vessel {args[0]}"
|
||||||
|
if len(users) > 1:
|
||||||
|
return f"98 Configuration Error: multiple users called {args[1]} found on vessel {args[0]}"
|
||||||
|
|
||||||
|
return "20 OK: " + json.dumps(list(users.values())[0])
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
self.user = False
|
||||||
|
|
||||||
|
self.request.sendall("ReportMonster Server\n(c) Kumi Systems e.U. (https://kumi.systems/)\n\n> ".encode())
|
||||||
|
|
||||||
|
while True:
|
||||||
|
data = self.request.recv(1024)
|
||||||
|
if data:
|
||||||
|
plain = data.decode()
|
||||||
|
if not (parts := plain.split()):
|
||||||
|
response = "00 No Command Received"
|
||||||
|
|
||||||
|
else:
|
||||||
|
parts = plain.split()
|
||||||
|
|
||||||
|
command = parts[0]
|
||||||
|
args = parts[1:]
|
||||||
|
|
||||||
|
COMMANDS = {
|
||||||
|
"login": self.command_login,
|
||||||
|
"user": self.command_user,
|
||||||
|
"users": self.command_users,
|
||||||
|
"vessels": self.command_vessels,
|
||||||
|
"w": self.command_whoami,
|
||||||
|
"whoami": self.command_whoami,
|
||||||
|
}
|
||||||
|
|
||||||
|
if command in COMMANDS.keys():
|
||||||
|
response = COMMANDS[command](*args)
|
||||||
|
else:
|
||||||
|
response = f"91 Invalid Command: don't know command {command}"
|
||||||
|
|
||||||
|
self.request.sendall(response.encode())
|
||||||
|
self.request.sendall("\n> ".encode())
|
||||||
|
|
||||||
|
|
||||||
|
class ReportMonsterServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||||
|
allow_reuse_address = True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
HOST = "127.0.0.1"
|
||||||
|
PORT = 6789
|
||||||
|
|
||||||
|
CONFIG = MonsterConfig(pathlib.Path(__file__).resolve().parent / "settings.ini")
|
||||||
|
|
||||||
|
server = ReportMonsterServer((HOST, PORT), ReportMonsterHandler)
|
||||||
|
server.config = CONFIG
|
||||||
|
|
||||||
|
with server:
|
||||||
|
st = threading.Thread(target=server.serve_forever)
|
||||||
|
st.daemon = True
|
||||||
|
st.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while st.is_alive():
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Received Ctrl+C - stopping server...")
|
||||||
|
server.shutdown()
|
||||||
|
st.join()
|
|
@ -9,9 +9,11 @@ Database = moodle
|
||||||
ssh = 0
|
ssh = 0
|
||||||
|
|
||||||
[Vessel vessel2]
|
[Vessel vessel2]
|
||||||
Host = 192.168.95192.1
|
Host = 192.168.192.1
|
||||||
Username = monster
|
Username = monster
|
||||||
Password = jfaskldfjklasfh
|
Password = jfaskldfjklasfh
|
||||||
Database = moodle
|
Database = moodle
|
||||||
ssh = 1
|
ssh = 1
|
||||||
|
|
||||||
|
[User test]
|
||||||
|
Password = test
|
Loading…
Reference in a new issue