feat: session auth + per-user/persona UI at /{user}/{persona}
Replaces nginx basic auth with a proper per-user session system:
- auth_utils.py: bcrypt password hashing, JWT cookie creation/decode
- auth_middleware.py: validates JWT cookie on all routes except /login,
/health, /static/, and webhook endpoints (/channels/, /webhook/)
- routers/ui.py: GET /login, POST /login, POST /logout,
GET /{username}/{persona} — serves index.html with CORTEX_CONFIG injected
- static/login.html: minimal login form (dark theme, matches UI)
- main.py: registers SessionAuthMiddleware + ui.router
- config.py: jwt_secret, jwt_expire_days settings
- manage_passwords.py: CLI tool to set/check/list user passwords
- app.js: reads window.CORTEX_CONFIG (user + persona), sends both on
every /chat and /orchestrate request; persona name shown in header;
logout button (⏏) added to header
- requirements.txt: bcrypt, PyJWT, python-multipart
- .env.default: JWT_SECRET, JWT_EXPIRE_DAYS documented
- tests: client fixture injects JWT cookie; security test assertions
updated for URL-normalized path traversal paths (still secure, codes differ)
All 80 tests pass.
Setup for a new user:
python manage_passwords.py set scott
python manage_passwords.py set holly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
76
cortex/manage_passwords.py
Normal file
76
cortex/manage_passwords.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Password management for Cortex users.
|
||||
|
||||
Usage:
|
||||
python manage_passwords.py set <username> # prompt for password
|
||||
python manage_passwords.py set <username> <pass> # set directly (avoid in shell history)
|
||||
python manage_passwords.py check <username> # test a password interactively
|
||||
python manage_passwords.py list # show which users have a password set
|
||||
"""
|
||||
|
||||
import sys
|
||||
import getpass
|
||||
|
||||
# Add cortex/ to path so we can import config and auth_utils
|
||||
sys.path.insert(0, str(__import__('pathlib').Path(__file__).parent))
|
||||
|
||||
from auth_utils import set_password, check_credentials, _auth_path
|
||||
from persona import list_users
|
||||
|
||||
|
||||
def cmd_set(args):
|
||||
if not args:
|
||||
print("Usage: manage_passwords.py set <username> [password]")
|
||||
sys.exit(1)
|
||||
username = args[0]
|
||||
if len(args) >= 2:
|
||||
password = args[1]
|
||||
else:
|
||||
password = getpass.getpass(f"New password for {username}: ")
|
||||
confirm = getpass.getpass("Confirm password: ")
|
||||
if password != confirm:
|
||||
print("Passwords do not match.")
|
||||
sys.exit(1)
|
||||
set_password(username, password)
|
||||
print(f"Password set for: {username}")
|
||||
|
||||
|
||||
def cmd_check(args):
|
||||
if not args:
|
||||
print("Usage: manage_passwords.py check <username>")
|
||||
sys.exit(1)
|
||||
username = args[0]
|
||||
password = getpass.getpass(f"Password for {username}: ")
|
||||
if check_credentials(username, password):
|
||||
print("OK — credentials are valid.")
|
||||
else:
|
||||
print("FAIL — invalid username or password.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_list(_args):
|
||||
for user in list_users():
|
||||
has = _auth_path(user).exists()
|
||||
status = "✓ password set" if has else "✗ no password"
|
||||
print(f" {user:<20} {status}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
sys.exit(0)
|
||||
|
||||
command = sys.argv[1]
|
||||
rest = sys.argv[2:]
|
||||
|
||||
if command == "set":
|
||||
cmd_set(rest)
|
||||
elif command == "check":
|
||||
cmd_check(rest)
|
||||
elif command == "list":
|
||||
cmd_list(rest)
|
||||
else:
|
||||
print(f"Unknown command: {command}")
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user