Files
max/max/db/users.py
T
finnandClaude Opus 5.5 d33821dd1b Log security relevant events to an audit log
Successful logins, user creation and deletion, password changes and the
enabled and admin toggles are logged with actor, target and IP. So are
failed logins, a wrong current password and denied access. Values are
JSON encoded, so client input cannot break the line or add fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:55:49 +00:00

135 lines
3.8 KiB
Python

from dataclasses import dataclass, fields
from .common import NoteLinesMixin, create_email
from .connection import with_cursor
@dataclass
class User(NoteLinesMixin):
id: int
email_id: int
email: str
passwordhash: str
enabled: bool
is_admin: bool
can_use_different_alias_domain: bool
note: str
select_fields = ", ".join(
map(
lambda field: f"{'u' if field.name != 'email' else 'e'}.{field.name}",
fields(User),
)
)
base_user_query = (
f"SELECT {select_fields} FROM users u INNER JOIN emails e ON u.email_id=e.id"
)
@with_cursor
def get_user_by_email(cur, email):
return get_user(cur, f"{base_user_query} WHERE e.email=lower(%s)", email)
@with_cursor
def get_user_by_user_id(cur, user_id):
return get_user(cur, f"{base_user_query} WHERE u.id=%s", user_id)
def get_user(cur, query, param):
cur.execute(query, [param])
row = cur.fetchone()
if not row:
return None
return User(*row)
@with_cursor
def get_users(cur):
cur.execute(f"{base_user_query} ORDER BY e.email ASC")
return [User(*row) for row in cur.fetchall()]
@with_cursor
def update_user(cur, id, **fields):
"""only pwhash, enabled, is_admin, can_use_different_alias_domain, note"""
cur.execute(
"UPDATE users SET "
+ ", ".join(f"{field}=%s" for field in fields.keys())
+ " WHERE id=%s",
list(fields.values()) + [id],
)
@with_cursor
def create_user(
cur, email, passwordhash, enabled, is_admin, can_use_different_alias_domain, note
):
"""Returns the id of the new user"""
email_id = create_email(cur, email)
cur.execute(
"""\
INSERT INTO users
(email_id, passwordhash, enabled, is_admin, can_use_different_alias_domain, note)
VALUES (%s, %s, %s, %s, %s, %s) RETURNING id""",
[
email_id,
passwordhash,
enabled,
is_admin,
can_use_different_alias_domain,
note,
],
)
return cur.fetchone()[0]
@with_cursor
def delete_user(cur, user):
cur.execute("DELETE FROM users WHERE id=%s", [user.id])
cur.execute("DELETE FROM emails WHERE id=%s", [user.email_id])
@with_cursor
def reset_or_create_user(cur, email, hash, is_admin):
# does the email exists?
cur.execute("SELECT id FROM emails WHERE email=%s", [email])
email_id = cur.fetchone()
if email_id is not None:
email_id = email_id[0]
# alias or user?
cur.execute("SELECT id FROM users WHERE email_id=%s", [email_id])
user_id = cur.fetchone()
if user_id is not None:
user_id = user_id[0]
# update User
cur.execute(
"UPDATE users SET passwordhash=%s, is_admin=%s, enabled=%s WHERE id=%s",
[hash, is_admin, True, user_id],
)
print(
f"reset password for {email}. Set admin={is_admin} and enabled the user."
)
else:
print(f"{email} is used as an alias. Please specify another one.")
else:
# user does not exists. create him.
cur.execute("INSERT INTO emails (email) VALUES (%s) RETURNING id", [email])
email_id = cur.fetchone()[0]
cur.execute(
"INSERT INTO users (email_id, passwordhash, enabled, is_admin, can_use_different_alias_domain, note) VALUES (%s, %s, %s, %s, %s, %s)", # noqa
[email_id, hash, True, is_admin, is_admin, "Created by commandline"],
)
user_type = "admin" if is_admin else "user"
print(f"Created {user_type} {email}")
@with_cursor
def is_last_enabled_admin(cur, user_id):
cur.execute(
"SELECT EXISTS (SELECT 1 FROM users u WHERE u.id != %s AND u.is_admin AND u.enabled)",
[user_id],
)
return not cur.fetchone()[0]