Compare commits
2 Commits
2025d26a5c
...
b6a29b1842
| Author | SHA1 | Date | |
|---|---|---|---|
| b6a29b1842 | |||
| 842ad3ff44 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -11,7 +11,8 @@
|
|||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix",
|
"nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix",
|
||||||
"python.testing.unittestArgs": ["-s", "simple_chat_api/tests", "-p", "*.py"],
|
"python.testing.unittestArgs": ["-s", ".", "-p", "*.py"],
|
||||||
"python.testing.pytestEnabled": false,
|
"python.testing.pytestEnabled": false,
|
||||||
|
"python.testing.cwd": "${workspaceFolder}",
|
||||||
"python.testing.unittestEnabled": true
|
"python.testing.unittestEnabled": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ This is a simple one-room chat application with user management, developed as a
|
|||||||
|
|
||||||
**Not recommended for production use.**
|
**Not recommended for production use.**
|
||||||
|
|
||||||
A test protocol can be found here: [simple_chat_testprotokoll.pdf](simple_chat_testprotokoll.pdf)
|
A test protocol can be found here: [docs/simple_chat_testprotokoll.pdf](simple_chat_testprotokoll.pdf)
|
||||||
|
|
||||||
## Running with Docker
|
## Running with Docker
|
||||||
|
|
||||||
|
|||||||
BIN
docs/unittest_sample.sqlite
Normal file
BIN
docs/unittest_sample.sqlite
Normal file
Binary file not shown.
@ -5,6 +5,7 @@ import { type User, primaryUser } from '@/composable/auth.ts'
|
|||||||
import { deleteUser, addUser, changePassword } from '@/composable/settings'
|
import { deleteUser, addUser, changePassword } from '@/composable/settings'
|
||||||
|
|
||||||
import UserInfo from '@/components/UserInfo.vue'
|
import UserInfo from '@/components/UserInfo.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
const new_user_name = ref('')
|
const new_user_name = ref('')
|
||||||
const new_user_passwd = ref('')
|
const new_user_passwd = ref('')
|
||||||
@ -50,6 +51,20 @@ const onChangePassword = async () => {
|
|||||||
<h3>User Profile</h3>
|
<h3>User Profile</h3>
|
||||||
<p><a class="font-bold">Name:</a> {{ primaryUser.currentUser.value.user }}</p>
|
<p><a class="font-bold">Name:</a> {{ primaryUser.currentUser.value.user }}</p>
|
||||||
<p><a class="font-bold">Role:</a> {{ primaryUser.currentUser.value.role }}</p>
|
<p><a class="font-bold">Role:</a> {{ primaryUser.currentUser.value.role }}</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
() =>
|
||||||
|
deleteUser(primaryUser.getSessionFromJWT()).then(() => {
|
||||||
|
primaryUser.removeToken()
|
||||||
|
router.push('login')
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Delete Profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="boxed w-80">
|
<div class="boxed w-80">
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
from simple_chat_api.main import main
|
from simple_chat_api.main import main
|
||||||
|
from simple_chat_api.db_handler.db_handler import DbConnector
|
||||||
0
simple_chat_api/db_handler/__init__.py
Normal file
0
simple_chat_api/db_handler/__init__.py
Normal file
@ -34,6 +34,8 @@ class DbConnector:
|
|||||||
self.session = sessionmaker(bind=self.engine)()
|
self.session = sessionmaker(bind=self.engine)()
|
||||||
self._create_defaults()
|
self._create_defaults()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.session.close()
|
||||||
|
|
||||||
def _create_optional_default_user(self):
|
def _create_optional_default_user(self):
|
||||||
for user in optional_default_user:
|
for user in optional_default_user:
|
||||||
71
simple_chat_api/db_handler/user_manager.py
Normal file
71
simple_chat_api/db_handler/user_manager.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
from simple_chat_api.db_handler.db_handler import DbConnector
|
||||||
|
from simple_chat_api.config import JWT_SECRET, hash_context
|
||||||
|
|
||||||
|
class UserManager:
|
||||||
|
"""
|
||||||
|
Class that handles authentication with database backend
|
||||||
|
"""
|
||||||
|
def __init__(self, db_con: DbConnector):
|
||||||
|
self.db_con = db_con
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(self, username: str, password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Create a new user in the database
|
||||||
|
:param username: logon name
|
||||||
|
:param password: password to save
|
||||||
|
:return: True if user was created
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.db_con.add_user(username, password)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def authenticate(self, username: str, password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Authenticate against the database
|
||||||
|
:param username: username to check
|
||||||
|
:param password: password to check
|
||||||
|
:return: True if authenticated
|
||||||
|
"""
|
||||||
|
user = self.db_con.get_user(username)
|
||||||
|
if not user or not hash_context.verify(password, user.hash):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def delete_user(self, username: str) -> bool:
|
||||||
|
"""
|
||||||
|
Delete a user from the database
|
||||||
|
:param username: username to delete
|
||||||
|
:return: True on success
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.db_con.delete_user(username)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def change_user_password(self, username: str, password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Change the password for a user
|
||||||
|
:param username: username to change
|
||||||
|
:param password: new password
|
||||||
|
:return: True on success
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.db_con.change_password(username, hash_context.hash(password))
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_user_exists(self, username: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a user exists in the database
|
||||||
|
:param username: logon name to look for
|
||||||
|
:return: True if user exists
|
||||||
|
"""
|
||||||
|
if self.db_con.get_user(username):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
@ -127,9 +127,16 @@ def change_password(user):
|
|||||||
return dumps({"error": str(e)})
|
return dumps({"error": str(e)})
|
||||||
|
|
||||||
@app.route("/delete/<deletion_target>", method=["POST"])
|
@app.route("/delete/<deletion_target>", method=["POST"])
|
||||||
@admin_guard()
|
@user_guard()
|
||||||
def delete_user(_, deletion_target: str):
|
def delete_user(user, deletion_target: str):
|
||||||
response.content_type = 'application/json'
|
response.content_type = 'application/json'
|
||||||
|
is_admin = admin_guard()(lambda _: True)() == True
|
||||||
|
# Note: we could just use user.role == "admin" but we
|
||||||
|
# want to follow potential requirement changes in the decorator
|
||||||
|
if user.name != deletion_target and not is_admin:
|
||||||
|
response.status = 401
|
||||||
|
return dumps({"error": "Not permited"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request.db_connector.delete_user(deletion_target)
|
request.db_connector.delete_user(deletion_target)
|
||||||
response.status = 200
|
response.status = 200
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
from bottle import Bottle, request, response
|
from bottle import Bottle, request, response
|
||||||
from json import dumps, loads
|
from json import dumps, loads
|
||||||
|
|
||||||
from simple_chat_api.db_handler import User, Message
|
from simple_chat_api.db_handler.db_handler import User, Message
|
||||||
from simple_chat_api.endpoints.auth import user_guard
|
from simple_chat_api.endpoints.auth import user_guard
|
||||||
from simple_chat_api.utils import read_keys_from_request
|
from simple_chat_api.utils import read_keys_from_request
|
||||||
app = Bottle()
|
app = Bottle()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from bottle import request, Bottle
|
from bottle import request, Bottle
|
||||||
|
|
||||||
from simple_chat_api.db_handler import DbConnector
|
from simple_chat_api.db_handler.db_handler import DbConnector
|
||||||
|
|
||||||
import simple_chat_api.endpoints.auth as auth
|
import simple_chat_api.endpoints.auth as auth
|
||||||
import simple_chat_api.endpoints.messages as messages
|
import simple_chat_api.endpoints.messages as messages
|
||||||
|
|||||||
0
simple_chat_api/tests/__init__.py
Normal file
0
simple_chat_api/tests/__init__.py
Normal file
@ -160,7 +160,7 @@ class TestAuthEndpoints(unittest.TestCase):
|
|||||||
else:
|
else:
|
||||||
self.assertEqual(response.status_code, 400, f"Non-admin user {user} should not create users; {response.text}")
|
self.assertEqual(response.status_code, 400, f"Non-admin user {user} should not create users; {response.text}")
|
||||||
|
|
||||||
def test_5_delete_user(self):
|
def test_6_delete_user(self):
|
||||||
if self.userSessions == {}:
|
if self.userSessions == {}:
|
||||||
self.skipTest("No user sessions available. Run test_get_token first.")
|
self.skipTest("No user sessions available. Run test_get_token first.")
|
||||||
|
|
||||||
@ -175,10 +175,14 @@ class TestAuthEndpoints(unittest.TestCase):
|
|||||||
response = session.post(f"{API_URL}/user/delete/nonexistent")
|
response = session.post(f"{API_URL}/user/delete/nonexistent")
|
||||||
self.assertEqual(response.status_code, 400, f"Deleting non-existent user should fail; {response.text}")
|
self.assertEqual(response.status_code, 400, f"Deleting non-existent user should fail; {response.text}")
|
||||||
else:
|
else:
|
||||||
response = session.post(f"{API_URL}/user/delete/max")
|
response = session.post(f"{API_URL}/user/delete/ina")
|
||||||
self.assertEqual(response.status_code, 401, f"Non-admin user {user} should not delete users; {response.text}")
|
self.assertEqual(response.status_code, 401, f"Non-admin user {user} should not delete users; {response.text}")
|
||||||
|
response = session.post(f"{API_URL}/user/delete/{user}")
|
||||||
|
self.assertEqual(response.status_code, 200, f"Non-admin user {user} should be able to delete self; {response.text}")
|
||||||
|
|
||||||
def test_6_change_password(self):
|
|
||||||
|
|
||||||
|
def test_5_change_password(self):
|
||||||
if self.userSessions == {}:
|
if self.userSessions == {}:
|
||||||
self.skipTest("No user sessions available. Run test_get_token first.")
|
self.skipTest("No user sessions available. Run test_get_token first.")
|
||||||
|
|
||||||
|
|||||||
46
simple_chat_api/tests/user_manager_unittest.py
Normal file
46
simple_chat_api/tests/user_manager_unittest.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from simple_chat_api.db_handler.db_handler import DbConnector
|
||||||
|
from simple_chat_api.db_handler.user_manager import UserManager
|
||||||
|
|
||||||
|
DB_PATH = "./data/TEST.sqlite"
|
||||||
|
|
||||||
|
EXISTING_USER = ("admin", "admin")
|
||||||
|
|
||||||
|
class TestUserManagmentWrapper(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
shutil.copyfile("docs/unittest_sample.sqlite", DB_PATH)
|
||||||
|
db = DbConnector(f"sqlite:///{DB_PATH}")
|
||||||
|
self.user_mgr = UserManager(db)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.user_mgr.db_con.close()
|
||||||
|
os.remove(DB_PATH)
|
||||||
|
|
||||||
|
def test_create_user(self):
|
||||||
|
user = ("someone", "some_pass")
|
||||||
|
ret = self.user_mgr.create_user(*user)
|
||||||
|
self.assertTrue(ret, f"user {user} faild to be created")
|
||||||
|
|
||||||
|
def test_authenticate(self):
|
||||||
|
self.assertTrue(self.user_mgr.authenticate(*EXISTING_USER),
|
||||||
|
f"Existing user {EXISTING_USER} could not be authenticated")
|
||||||
|
self.assertFalse(self.user_mgr.authenticate(EXISTING_USER[0], "unknown_pass"),
|
||||||
|
f"Existing user {EXISTING_USER[0]} could be authenticated with wrong password")
|
||||||
|
self.assertFalse(self.user_mgr.authenticate("unknown_user", "unknown_pass"),
|
||||||
|
f"An unknown User was authenticated")
|
||||||
|
|
||||||
|
def test_delete_user(self):
|
||||||
|
self.assertTrue(self.user_mgr.delete_user(EXISTING_USER[0]), f"The existing user {EXISTING_USER[0]} could not be removed")
|
||||||
|
self.assertFalse(self.user_mgr.delete_user("unknown_user"), "an unknown user could be deleted")
|
||||||
|
|
||||||
|
def test_change_password(self):
|
||||||
|
self.assertTrue(self.user_mgr.change_user_password(EXISTING_USER[0], "new_pass"), f"Could not change password for existing user")
|
||||||
|
self.assertFalse(self.user_mgr.change_user_password("unknown_user", "new_pass"), "The password for an unknown user was changed")
|
||||||
|
|
||||||
|
def test_check_user_exists(self):
|
||||||
|
self.assertTrue(self.user_mgr.check_user_exists(EXISTING_USER[0]), f"The {EXISTING_USER[0]} could not be found")
|
||||||
|
self.assertFalse(self.user_mgr.check_user_exists("unknown_user"), "An unknown user was found")
|
||||||
Loading…
x
Reference in New Issue
Block a user