Initial import: eCW Charge Validation bot and results Portal

Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one
commit with no earlier history: the tracked files were de-identified
first (synthetic claim ids and patient data only; see Context.md
section 7). Credentials, run logs, screenshots, claim reports and client
workbooks are gitignored and not included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ManikandanandClaude Opus 5.5 committed 2026-10-23 18:38:17 +05:30
commit ba15a574ab
205 files changed
+56772

No files matched your search

+328
View File
@@ -0,0 +1,328 @@
r"""
Portal Users, Sign-in and sessions (spec docs/superpowers/specs/2026-10-22-
run-tracking-and-portal-sign-in-design.md, Stage 2, EEWA-32; terms in
GLOSSARY.md).
* **Passwords** are stored only as PBKDF2-SHA256 hashes with their own
salt and iteration count (stdlib, no new package on this locked-down
PC); never stored, logged or shown in plain. At least 10 characters,
not the username.
* **Roles**: Admin (everything, and managing Portal Users), Operator (see
everything, Start / Stop a Run), Viewer (see only).
* **Lockout**: 5 wrong passwords in a row lock the user for 15 minutes.
An unknown username is told the same as a wrong password, and costs the
same time (a hash is still computed), so the page does not reveal which
usernames exist.
* **Sessions** live in the Portal's memory: a server restart signs
everyone out. 30 minutes idle ends one. Disabling a user, resetting
their password or changing their role ends theirs at once.
* Users are disabled, never deleted; their history stays.
* Every Sign-in, Sign-out, wrong password, lock, timeout and Admin change
is a row in `CV_PortalSignIn`.
`Accounts` is the logic, against any store with the `SqlUserStore`
methods (tests use an in-memory one).
"""
import re
import hmac
import secrets
import hashlib
from datetime import datetime, timedelta
ROLES = ("Admin", "Operator", "Viewer")
CAN_RUN = ("Admin", "Operator")
MIN_PASSWORD = 10
ITERATIONS = 600_000
LOCK_AFTER = 5
LOCK_MINUTES = 15
IDLE_MINUTES = 30
USERNAME = re.compile(r"^[A-Za-z0-9._-]{2,50}$")
USER_TABLE = "dbo.CV_PortalUser"
SIGNIN_TABLE = "dbo.CV_PortalSignIn"
_USER_COLUMNS = ("username", "display_name", "role", "pw_hash", "pw_salt",
"pw_iterations", "must_change", "disabled", "failed_count",
"locked_until", "created_at", "created_by", "changed_at")
class UserError(ValueError):
"""A request an Admin or user can fix: the message says how."""
# ── Passwords ─────────────────────────────────────────────────────────────────
def hash_password(password: str, salt: str = None,
iterations: int = None) -> tuple:
"""(hash hex, salt hex, iterations)."""
salt = salt or secrets.token_hex(16)
iterations = iterations or ITERATIONS
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"),
bytes.fromhex(salt), iterations)
return digest.hex(), salt, iterations
def verify_password(password: str, pw_hash: str, salt: str,
iterations: int) -> bool:
try:
got, _, _ = hash_password(password, salt, int(iterations))
except (ValueError, TypeError):
return False
return hmac.compare_digest(got, pw_hash or "")
def password_problem(password: str, username: str = ""):
"""Why `password` is not allowed, or None."""
if len(password or "") < MIN_PASSWORD:
return f"The password needs at least {MIN_PASSWORD} characters"
if username and password.strip().lower() == username.lower():
return "The password cannot be the username"
return None
def clean_username(username: str) -> str:
return (username or "").strip().lower()
# ── The store (SQL Server) ────────────────────────────────────────────────────
class SqlUserStore:
"""`CV_PortalUser` and `CV_PortalSignIn` in Charge_Validation. Every
statement parameterized; one short connection per call."""
def __init__(self, conn_str: str, connect=None):
self.conn_str = conn_str
self._connect = connect
def _run(self, sql: str, params=(), fetch=False):
connect = self._connect
if connect is None:
import pyodbc
connect = pyodbc.connect
conn = connect(self.conn_str, timeout=10)
try:
cur = conn.cursor().execute(sql, list(params))
if fetch:
cols = [d[0] for d in cur.description]
return [dict(zip(cols, row)) for row in cur.fetchall()]
conn.commit()
return None
finally:
conn.close()
def get(self, username):
rows = self._run(f"SELECT {', '.join(_USER_COLUMNS)} FROM "
f"{USER_TABLE} WHERE username = ?",
[username], fetch=True)
return _bools(rows[0]) if rows else None
def list(self) -> list:
return [_bools(r) for r in self._run(
f"SELECT {', '.join(_USER_COLUMNS)} FROM {USER_TABLE} "
f"ORDER BY username", fetch=True)]
def create(self, user: dict) -> None:
cols = [c for c in _USER_COLUMNS if c in user]
self._run(f"INSERT INTO {USER_TABLE} ({', '.join(cols)}) VALUES "
f"({', '.join('?' for _ in cols)})",
[user[c] for c in cols])
def update(self, username, **fields) -> None:
cols = [c for c in fields if c in _USER_COLUMNS and c != "username"]
if not cols:
return
self._run(f"UPDATE {USER_TABLE} SET "
f"{', '.join(c + ' = ?' for c in cols)} WHERE username = ?",
[fields[c] for c in cols] + [username])
def record(self, username, event, by="") -> None:
self._run(f"INSERT INTO {SIGNIN_TABLE} (at, username, event, by_user)"
f" VALUES (?, ?, ?, ?)",
[datetime.now().replace(microsecond=0), username[:50],
event, by or None])
def history(self, limit: int = 300) -> list:
return self._run(f"SELECT TOP {int(limit)} at, username, event, "
f"by_user FROM {SIGNIN_TABLE} ORDER BY id DESC",
fetch=True)
def _bools(row: dict) -> dict:
for k in ("must_change", "disabled"):
row[k] = bool(row.get(k))
return row
# ── Sessions ──────────────────────────────────────────────────────────────────
class Sessions:
"""Signed-in users by cookie token, in memory."""
def __init__(self, now=datetime.now):
self._now = now
self._by_token = {}
def new(self, user: dict) -> str:
token = secrets.token_urlsafe(32)
self._by_token[token] = {"user": _public(user), "last": self._now()}
return token
def get(self, token):
"""(user dict, None) for a live session; (None, username) for one
that just timed out; (None, None) otherwise."""
s = self._by_token.get(token or "")
if not s:
return None, None
if self._now() - s["last"] > timedelta(minutes=IDLE_MINUTES):
del self._by_token[token]
return None, s["user"]["username"]
s["last"] = self._now()
return s["user"], None
def drop(self, token) -> None:
self._by_token.pop(token or "", None)
def drop_user(self, username) -> None:
for t in [t for t, s in self._by_token.items()
if s["user"]["username"] == username]:
del self._by_token[t]
def refresh_user(self, user: dict) -> None:
for s in self._by_token.values():
if s["user"]["username"] == user["username"]:
s["user"] = _public(user)
def _public(user: dict) -> dict:
"""What a session keeps: never the hash."""
return {"username": user["username"],
"display_name": user.get("display_name") or user["username"],
"role": user["role"], "must_change": bool(user.get("must_change"))}
# ── The rules ─────────────────────────────────────────────────────────────────
class Accounts:
def __init__(self, store, now=datetime.now):
self.store = store
self._now = now
# Spent on an unknown username so it takes as long as a known one.
self._dummy = hash_password(secrets.token_hex(8))
def sign_in(self, username: str, password: str):
"""(user, "ok") or (None, "bad" | "locked" | "disabled")."""
name = clean_username(username)
user = self.store.get(name) if USERNAME.match(name) else None
if user is None:
verify_password(password or "", *self._dummy)
if name:
self.store.record(name, "wrong_password")
return None, "bad"
now = self._now()
if user.get("locked_until") and user["locked_until"] > now:
self.store.record(name, "locked")
return None, "locked"
if not verify_password(password or "", user["pw_hash"],
user["pw_salt"], user["pw_iterations"]):
failed = int(user.get("failed_count") or 0) + 1
if failed >= LOCK_AFTER:
self.store.update(name, failed_count=0, locked_until=now
+ timedelta(minutes=LOCK_MINUTES))
self.store.record(name, "locked")
return None, "locked"
self.store.update(name, failed_count=failed)
self.store.record(name, "wrong_password")
return None, "bad"
if user.get("disabled"):
self.store.record(name, "disabled_attempt")
return None, "disabled"
self.store.update(name, failed_count=0, locked_until=None)
self.store.record(name, "sign_in")
return user, "ok"
def create_user(self, username, display_name, role, password, by=""):
name = clean_username(username)
if not USERNAME.match(name):
raise UserError("Username: 2-50 letters, digits, dot, dash or "
"underscore")
if role not in ROLES:
raise UserError(f"Role must be one of {', '.join(ROLES)}")
problem = password_problem(password, name)
if problem:
raise UserError(problem)
if self.store.get(name):
raise UserError(f"{name} already exists")
pw_hash, salt, iterations = hash_password(password)
now = self._now().replace(microsecond=0)
self.store.create({
"username": name,
"display_name": (display_name or "").strip()[:100] or name,
"role": role, "pw_hash": pw_hash, "pw_salt": salt,
"pw_iterations": iterations,
# Made by an Admin: the user picks their own at first Sign-in.
"must_change": bool(by), "disabled": False, "failed_count": 0,
"locked_until": None, "created_at": now,
"created_by": by or "add_portal_user", "changed_at": now})
self.store.record(name, "user_created", by)
return self.store.get(name)
def change_password(self, username, current, new):
user = self.store.get(username)
if not user or not verify_password(current or "", user["pw_hash"],
user["pw_salt"],
user["pw_iterations"]):
raise UserError("The current password is not right")
if current == new:
raise UserError("The new password must be different")
self._set_password(user, new, must_change=False)
self.store.record(username, "password_changed")
return self.store.get(username)
def reset_password(self, username, new, by):
user = self._existing(username)
self._set_password(user, new, must_change=True)
self.store.record(user["username"], "password_reset", by)
def set_role(self, username, role, by):
user = self._existing(username)
if role not in ROLES:
raise UserError(f"Role must be one of {', '.join(ROLES)}")
if user["role"] == "Admin" and role != "Admin":
self._keep_an_admin(user["username"])
self.store.update(user["username"], role=role,
changed_at=self._now().replace(microsecond=0))
self.store.record(user["username"], f"role_{role.lower()}", by)
def set_disabled(self, username, disabled: bool, by):
user = self._existing(username)
if disabled and user["username"] == by:
raise UserError("You cannot disable yourself")
if disabled and user["role"] == "Admin":
self._keep_an_admin(user["username"])
self.store.update(user["username"], disabled=disabled,
failed_count=0, locked_until=None,
changed_at=self._now().replace(microsecond=0))
self.store.record(user["username"],
"user_disabled" if disabled else "user_enabled", by)
def _set_password(self, user, new, must_change):
problem = password_problem(new, user["username"])
if problem:
raise UserError(problem)
pw_hash, salt, iterations = hash_password(new)
self.store.update(user["username"], pw_hash=pw_hash, pw_salt=salt,
pw_iterations=iterations, must_change=must_change,
failed_count=0, locked_until=None,
changed_at=self._now().replace(microsecond=0))
def _existing(self, username):
user = self.store.get(clean_username(username))
if not user:
raise UserError(f"No user {username}")
return user
def _keep_an_admin(self, leaving):
others = [u for u in self.store.list() if u["role"] == "Admin"
and not u["disabled"] and u["username"] != leaving]
if not others:
raise UserError("The Portal needs at least one active Admin")