Source code for yasuki_core.accounts.banlist
import psycopg
from yasuki_core.accounts.crypto import email_blind_index, sub_blind_index
[docs]
def tombstone(
conn: psycopg.Connection, google_sub: str, email_hmac: bytes, reason: str | None = None
) -> None:
"""Record a banned identity's pepper'd sub and email hashes so it cannot re-register.
The tombstone outlives the user row (it survives GDPR erasure), holding only hashes — no raw
PII. Re-banning the same identity refreshes the email hash, reason, and timestamp.
Parameters
----------
conn : psycopg.Connection
An open accounts-database connection.
google_sub : str
The banned account's Google subject identifier; stored only as its blind index.
email_hmac : bytes
The account's already-computed email blind index, as held on the user row.
reason : str, optional
A free-text ban reason. Default None.
"""
with conn.cursor() as cur:
cur.execute(
"INSERT INTO banlist (sub_hmac, email_hmac, reason) VALUES (%s, %s, %s) "
"ON CONFLICT (sub_hmac) DO UPDATE SET "
"email_hmac = EXCLUDED.email_hmac, reason = EXCLUDED.reason, banned_at = now()",
(sub_blind_index(google_sub), email_hmac, reason),
)
[docs]
def remove(conn: psycopg.Connection, google_sub: str) -> None:
"""Drop a tombstone by its Google sub, so the identity may sign in again (unban)."""
with conn.cursor() as cur:
cur.execute("DELETE FROM banlist WHERE sub_hmac = %s", (sub_blind_index(google_sub),))
[docs]
def is_banned(conn: psycopg.Connection, google_sub: str, email: str) -> bool:
"""Whether an incoming Google identity matches a banlist tombstone, by sub or by email.
Checked at signup so a banned person cannot return under a fresh account row — neither by
re-using the same Google account (sub match) nor a new one on the same address (email match).
Parameters
----------
conn : psycopg.Connection
An open accounts-database connection.
google_sub : str
The subject identifier from the verified id_token.
email : str
The address from the verified id_token.
Returns
-------
banned : bool
True if either hash is tombstoned.
"""
with conn.cursor() as cur:
cur.execute(
"SELECT 1 FROM banlist WHERE sub_hmac = %s OR email_hmac = %s LIMIT 1",
(sub_blind_index(google_sub), email_blind_index(email)),
)
return cur.fetchone() is not None