diff --git a/PROJECT_KNOWLEDGE.md b/PROJECT_KNOWLEDGE.md index 517bf4a..d3dc7a2 100644 --- a/PROJECT_KNOWLEDGE.md +++ b/PROJECT_KNOWLEDGE.md @@ -14,7 +14,7 @@ Backend: - `app.py` creates the Flask app. - HTTP Basic Auth is implemented with `Flask-HTTPAuth`. -- Users are currently hardcoded in `BETA_USERS`. +- Users are currently configured in `BETA_USERS` with Werkzeug password hashes. - `/` renders the active calculator template. - `/static/` is intended to serve static files behind Basic Auth. - `/api/health` returns health/version information. @@ -50,6 +50,24 @@ If `build_info.json` is missing, malformed, or does not contain a usable value, The data is loaded centrally in `app.py` and made available to every template as `build_info`. +## Authentication + +RollCalc uses HTTP Basic Auth. User entries in `BETA_USERS` have this shape: + +```python +"username": { + "password_hash": "..." +} +``` + +Password verification uses `werkzeug.security.check_password_hash`. New hashes or config snippets can be generated with: + +```bash +python scripts/manage_users.py username +``` + +Passwords and hashes must not be logged. + ## Roll Geometry Calculations assume an ideal cylindrical winding. @@ -210,7 +228,7 @@ Login/access logging remains part of RollCalc, but log viewing and article-data ## Known Technical Risks -- Hardcoded plaintext credentials in `app.py`. +- Password hashes are currently configured in `app.py`; this should eventually move to a protected external configuration or secrets mechanism. - Basic Auth only; no sessions or role framework beyond the user dictionary. - `access_log.json` is rewritten on every logged request and is not concurrency-safe. - No log rotation or retention policy is implemented. diff --git a/README.md b/README.md index 914894c..e010721 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ If port `5000` is already occupied, start through Flask's CLI without changing f flask --app app run --host 127.0.0.1 --port 5001 ``` -The app uses HTTP Basic Auth. Current credentials are defined in `BETA_USERS` in `app.py`. +The app uses HTTP Basic Auth. Current users are defined in `BETA_USERS` in `app.py` with Werkzeug password hashes. ## Project Layout @@ -90,7 +90,22 @@ Implemented routes: | `/api/health` | `GET` | Basic Auth | Returns app health and version. | | `/api/user` | `GET` | Basic Auth | Returns current authenticated user info. | -Authentication is implemented with `Flask-HTTPAuth`. The current code checks `BETA_USERS` and performs direct plaintext string comparison. +Authentication is implemented with `Flask-HTTPAuth`. The current code checks `BETA_USERS` with `werkzeug.security.check_password_hash`; plaintext passwords are not stored in the application. + +Generate a password hash or user entry with: + +```bash +python scripts/manage_users.py username +python scripts/manage_users.py username --json +``` + +For non-interactive local maintenance only: + +```bash +python scripts/manage_users.py username --password 'new-password' +``` + +Do not commit real passwords or print them in logs. Access logging is handled by `log_access()`, which reads `access_log.json`, appends a record, and writes the whole file back. @@ -287,11 +302,11 @@ Utility script that updates relative frontend fetch/register paths to Flask-styl - There are duplicated or legacy-looking files with similar names. Before editing JavaScript under `static/`, confirm it is actually referenced by the active template. - The documentation under `docs/` contains deployment and feature notes, but some filenames and route assumptions may not match the current app exactly. - The Flask dev server is used for local development only. Production should use a WSGI server. +- Use `scripts/manage_users.py` to create password hashes when adding or rotating Basic Auth users. ## Known Risks and Maintenance Items -- Credentials are hardcoded in `app.py` and should be moved to environment variables or a secrets manager. -- Passwords are stored in plaintext and compared directly. +- Password hashes are currently configured in `app.py`; user configuration should eventually move to environment variables, a protected config file, or a secrets manager. - `access_log.json` is not safe for concurrent writes. - `access_log.json` grows without rotation or retention limits. - The custom `/static/` route is intended to protect static files, but Flask also creates a default static route unless disabled. Verify effective route behavior before relying on static-file protection. diff --git a/access_log.json b/access_log.json index 6f51800..9a43159 100644 --- a/access_log.json +++ b/access_log.json @@ -628,5 +628,12 @@ "endpoint": "/", "method": "GET", "status": 200 + }, + { + "timestamp": "2026-07-07T16:13:42.026433", + "username": "mtazl", + "endpoint": "/", + "method": "GET", + "status": 200 } ] \ No newline at end of file diff --git a/app.py b/app.py index 76f3c24..1c250a6 100644 --- a/app.py +++ b/app.py @@ -5,6 +5,7 @@ Flask app with HTTP Basic Authentication from flask import Flask, render_template, request, send_file, send_from_directory, jsonify from flask_httpauth import HTTPBasicAuth +from werkzeug.security import check_password_hash from functools import wraps import os from datetime import datetime @@ -19,12 +20,24 @@ auth = HTTPBasicAuth() # Benutzer für Beta-Phase (in Produktion aus env-Variablen laden!) BETA_USERS = { - "beta": "rollcalc_beta_2026", # ⚠️ ÄNDERN! - "naue": "naue_access_2026", # ⚠️ ÄNDERN! - "cniehues": "beta_test_2026", # ⚠️ ÄNDERN! - "lvollmert": "Geheim!", # ⚠️ ÄNDERN! - "mtazl": "rollcalc", # ⚠️ ÄNDERN! - "controlling": "beta2026" # ⚠️ ÄNDERN! + "beta": { + "password_hash": "pbkdf2:sha256:600000$HNtF3VdZKdmtg8Vw$8f976a99a457c5e924916dc6f735dd631042c8c126af8d4bda9abcd7532d904f" + }, + "naue": { + "password_hash": "pbkdf2:sha256:600000$RVPiW2mYXJJIp3d7$0f838b8619d386e2da57e60ae8ccb944bb0f9f63d81ce3d174ec3034b0abcd19" + }, + "cniehues": { + "password_hash": "pbkdf2:sha256:600000$iNmhakf34xk26xrz$ae78846461370c52b7f56fad5ac0ae8e33860d6f00075ac78e3a5b8c31d51a3d" + }, + "lvollmert": { + "password_hash": "pbkdf2:sha256:600000$8FGop5ymmHpuIqfK$8ba223e20c514cf6bc1297435a2e7e2be81668951297f0e01ad6a931718ad7de" + }, + "mtazl": { + "password_hash": "pbkdf2:sha256:600000$H19skoalhWxlLnY5$d30bfeae38470b19900648fd110978b3677607c3a161d663a7e5d5c2167a3710" + }, + "controlling": { + "password_hash": "pbkdf2:sha256:600000$ksj86lrKz6nnSpXz$5161e0b5c76bd72d6b2cd04866ef49e69f463f474d7e47075171894bf3366f29" + } } # Logging für Auditing @@ -74,7 +87,12 @@ def inject_build_info(): @auth.verify_password def verify_password(username, password): """Verify HTTP Basic Auth credentials""" - if username in BETA_USERS and BETA_USERS[username] == password: + user_config = BETA_USERS.get(username) + if not user_config: + return None + + password_hash = user_config.get("password_hash") + if password_hash and check_password_hash(password_hash, password): return username return None diff --git a/scripts/manage_users.py b/scripts/manage_users.py new file mode 100644 index 0000000..2321f27 --- /dev/null +++ b/scripts/manage_users.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +""" +Create password hashes or user config snippets for RollCalc Basic Auth users. +""" + +import argparse +import getpass +import json + +from werkzeug.security import generate_password_hash + + +def build_parser(): + parser = argparse.ArgumentParser( + description="Generate RollCalc password hashes or user config entries." + ) + parser.add_argument( + "username", + nargs="?", + help="Optional username for a generated user config snippet.", + ) + parser.add_argument( + "--password", + help="Password to hash. Omit to enter it securely via prompt.", + ) + parser.add_argument( + "--json", + action="store_true", + help="Output a JSON object for the user entry.", + ) + return parser + + +def main(): + args = build_parser().parse_args() + password = args.password + + if password is None: + password = getpass.getpass("Password: ") + + password_hash = generate_password_hash(password) + + if args.username: + entry = {args.username: {"password_hash": password_hash}} + if args.json: + print(json.dumps(entry, indent=2)) + else: + print(f'"{args.username}": {{') + print(f' "password_hash": "{password_hash}"') + print("}") + else: + print(password_hash) + + +if __name__ == "__main__": + main()