From 974324e1b5ea112af4f44631f3f23f0270d359b6 Mon Sep 17 00:00:00 2001 From: Emilien Macchi Date: Mon, 12 May 2025 14:39:12 -0400 Subject: [PATCH] Add utils to create RCAccelerator users/tokens --- Containerfile | 1 + chatbot_db/README.md | 17 ++++++++++ chatbot_db/__init__.py | 0 chatbot_db/add_user.py | 77 ++++++++++++++++++++++++++++++++++++++++++ pdm.lock | 40 +++++++++++++++++++++- pyproject.toml | 2 ++ tox.ini | 4 +-- 7 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 chatbot_db/README.md create mode 100644 chatbot_db/__init__.py create mode 100644 chatbot_db/add_user.py diff --git a/Containerfile b/Containerfile index 2238ca4..df7fe6b 100644 --- a/Containerfile +++ b/Containerfile @@ -12,6 +12,7 @@ RUN chown -R tools:tools /app COPY feedback_exporter feedback_exporter COPY data_scraper data_scraper COPY evaluation evaluation +COPY chatbot_db chatbot_db COPY pdm.lock pyproject.toml Makefile . RUN make install-pdm install-global diff --git a/chatbot_db/README.md b/chatbot_db/README.md new file mode 100644 index 0000000..2494116 --- /dev/null +++ b/chatbot_db/README.md @@ -0,0 +1,17 @@ +# User and Token creation script for RCAccelerator + +This Python script creates a user and an associated access token in a PostgreSQL database, based on a schema with `users` and `tokens` tables. It is designed for RCAccelerator, to be interactive and secure, using bcrypt for password hashing. + +Run the script: + +```bash +python add_user.py +``` + +You will be prompted for: + +* The PostgreSQL DATABASE_URL (format: postgresql://user:pass@host:port/dbname) +* The username and email of the user +* The password (input hidden) + +A hashed password will be stored in the users table, and a 30-days token will be created in the tokens table. diff --git a/chatbot_db/__init__.py b/chatbot_db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chatbot_db/add_user.py b/chatbot_db/add_user.py new file mode 100644 index 0000000..b96eab3 --- /dev/null +++ b/chatbot_db/add_user.py @@ -0,0 +1,77 @@ +""" +Tool to add new users to the chatbot database with authentication tokens. +Handles user creation and token generation with secure password hashing. +""" +import uuid +import getpass +import sys +from datetime import datetime, timedelta, UTC + +import bcrypt +from sqlalchemy import create_engine, Column, String, TIMESTAMP, Table, MetaData, select, exc +from sqlalchemy.dialects.postgresql import UUID + +def main(): + """Entry point for chatbot_db module.""" + database_url = input("Enter your DATABASE URL " + "(e.g. postgresql://user:pass@host:port/db): ").strip() + username = input("Enter username: ").strip() + email = input("Enter email: ").strip() + password = getpass.getpass("Enter password (will be hashed): ") + password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + engine = create_engine(database_url) + metadata = MetaData() + users = Table( + "users", metadata, + Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4), + Column("username", String(150), nullable=False, unique=True), + Column("password_hash", String(255), nullable=False), + Column("email", String(150), nullable=False) + ) + tokens = Table( + "tokens", metadata, + Column("token", String(64), primary_key=True), + Column("username", String(50)), + Column("created_at", TIMESTAMP, default=datetime.now(UTC)), + Column("expires_at", TIMESTAMP, nullable=False) + ) + metadata.create_all(engine) + with engine.connect() as conn: + user_exists = conn.execute( + select(users.c.username).where(users.c.username == username) + ).fetchone() + + if user_exists: + print(f"Error: User '{username}' already exists. Please choose a different username.") + sys.exit(1) + + try: + with engine.begin() as conn: + user_id = uuid.uuid4() + conn.execute(users.insert().values( + id=user_id, + username=username, + password_hash=password_hash, + email=email + )) + + token_value = uuid.uuid4().hex + conn.execute(tokens.insert().values( + token=token_value, + username=username, + created_at=datetime.now(UTC), + expires_at=datetime.now(UTC) + timedelta(days=30) + )) + + print(f"\n User '{username}' created with token: {token_value}") + except exc.IntegrityError as e: + print("Error: Database integrity error occurred. User may already exist or " + "there's a constraint violation.") + print(f"Details: {str(e)}") + sys.exit(1) + except (ConnectionError, TimeoutError) as e: + print(f"Error: An unexpected error occurred: {str(e)}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/pdm.lock b/pdm.lock index 7316ebb..8838490 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:df11e293dd0f226387cc80c9feb53cf4c65c3b8f59486e6dd80dab479ea53c2b" +content_hash = "sha256:83e12f0c2fb890a16369d3923b9df9568e9ca76daf49dfc72fc96ae0e460c240" [[metadata.targets]] requires_python = "==3.12.*" @@ -55,6 +55,44 @@ files = [ {file = "astroid-3.0.3.tar.gz", hash = "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93"}, ] +[[package]] +name = "bcrypt" +version = "4.3.0" +requires_python = ">=3.8" +summary = "Modern password hashing for your software and your servers" +groups = ["default"] +files = [ + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, +] + [[package]] name = "beautifulsoup4" version = "4.13.4" diff --git a/pyproject.toml b/pyproject.toml index e7271d0..f822051 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "requests-kerberos>=0.15.0", "browser-cookie3>=0.19.1", "httpx-gssapi>=0.2.0", + "bcrypt>=4.3.0", ] requires-python = "==3.12.*" @@ -42,6 +43,7 @@ feedback_exporter = "feedback_exporter.export_feedback:main" evaluation = "evaluation.evaluation:main" osp_doc_scraper = "data_scraper.main:osp_doc_scraper" solutions_scraper = "data_scraper.main:solutions_scraper" +chatbot_db = "chatbot_db:main" [tool.setuptools.packages.find] include = ["data_scraper*", "feedback_exporter*"] diff --git a/tox.ini b/tox.ini index a50ee33..d250697 100644 --- a/tox.ini +++ b/tox.ini @@ -18,10 +18,10 @@ commands = description = run Pylint commands = {[testenv]commands} - pylint {posargs:./data_scraper ./feedback_exporter ./evaluation} + pylint {posargs:./data_scraper ./feedback_exporter ./evaluation ./chatbot_db} [testenv:ruff] description = run ruff commands = {[testenv]commands} - ruff check {posargs:./data_scraper ./feedback_exporter ./evaluation} + ruff check {posargs:./data_scraper ./feedback_exporter ./evaluation ./chatbot_db}