139 lines
4.2 KiB
Python
139 lines
4.2 KiB
Python
|
|
"""
|
||
|
|
TEE OFF ADMIN ACCESS BOOTSTRAP
|
||
|
|
---------------------------------------------------------------------------
|
||
|
|
FUNKSJON: Oppretter eller oppdaterer én administrator uten å påvirke andre.
|
||
|
|
Passord leses skjult fra terminalen, og 2FA kan genereres/roteres.
|
||
|
|
STATUS: Trygg erstatning for create_admin.py når flere admins skal eksistere.
|
||
|
|
---------------------------------------------------------------------------
|
||
|
|
"""
|
||
|
|
import argparse
|
||
|
|
import asyncio
|
||
|
|
import getpass
|
||
|
|
import sys
|
||
|
|
|
||
|
|
import asyncpg
|
||
|
|
import pyotp
|
||
|
|
from passlib.hash import pbkdf2_sha256
|
||
|
|
|
||
|
|
from env_config import get_database_url
|
||
|
|
|
||
|
|
DB_URL = get_database_url()
|
||
|
|
|
||
|
|
|
||
|
|
def parse_args() -> argparse.Namespace:
|
||
|
|
parser = argparse.ArgumentParser(description="Opprett eller oppdater en admin-bruker trygt.")
|
||
|
|
parser.add_argument("--username", required=True, help="Brukernavn for admin-brukeren")
|
||
|
|
parser.add_argument("--email", required=True, help="E-post for admin-brukeren")
|
||
|
|
parser.add_argument(
|
||
|
|
"--rotate-2fa",
|
||
|
|
action="store_true",
|
||
|
|
help="Generer en ny 2FA-hemmelighet selv om brukeren allerede har en",
|
||
|
|
)
|
||
|
|
return parser.parse_args()
|
||
|
|
|
||
|
|
|
||
|
|
async def bootstrap_admin() -> None:
|
||
|
|
args = parse_args()
|
||
|
|
username = args.username.strip()
|
||
|
|
email = args.email.strip().lower()
|
||
|
|
|
||
|
|
if not username:
|
||
|
|
print("❌ Brukernavn kan ikke være tomt.")
|
||
|
|
sys.exit(1)
|
||
|
|
if "@" not in email:
|
||
|
|
print("❌ E-postadressen ser ugyldig ut.")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
while True:
|
||
|
|
password = getpass.getpass("Skriv inn passord: ")
|
||
|
|
password_confirm = getpass.getpass("Gjenta passord: ")
|
||
|
|
|
||
|
|
if password != password_confirm:
|
||
|
|
print("❌ Passordene er ikke like. Prøv igjen.\n")
|
||
|
|
continue
|
||
|
|
|
||
|
|
if len(password) < 8:
|
||
|
|
print("⚠️ Advarsel: Passordet bør være minst 8 tegn.")
|
||
|
|
break
|
||
|
|
|
||
|
|
password_hash = pbkdf2_sha256.hash(password)
|
||
|
|
|
||
|
|
conn = None
|
||
|
|
try:
|
||
|
|
conn = await asyncpg.connect(DB_URL)
|
||
|
|
existing = await conn.fetchrow(
|
||
|
|
"""
|
||
|
|
SELECT id, username, email, otp_secret
|
||
|
|
FROM admins
|
||
|
|
WHERE username = $1 OR email = $2
|
||
|
|
ORDER BY id ASC
|
||
|
|
LIMIT 1
|
||
|
|
""",
|
||
|
|
username,
|
||
|
|
email,
|
||
|
|
)
|
||
|
|
|
||
|
|
otp_secret = pyotp.random_base32() if (args.rotate_2fa or not existing or not existing["otp_secret"]) else existing["otp_secret"]
|
||
|
|
|
||
|
|
if existing:
|
||
|
|
await conn.execute(
|
||
|
|
"""
|
||
|
|
UPDATE admins
|
||
|
|
SET username = $1,
|
||
|
|
email = $2,
|
||
|
|
password_hash = $3,
|
||
|
|
otp_secret = $4
|
||
|
|
WHERE id = $5
|
||
|
|
""",
|
||
|
|
username,
|
||
|
|
email,
|
||
|
|
password_hash,
|
||
|
|
otp_secret,
|
||
|
|
existing["id"],
|
||
|
|
)
|
||
|
|
action = "oppdatert"
|
||
|
|
else:
|
||
|
|
await conn.execute(
|
||
|
|
"""
|
||
|
|
INSERT INTO admins (username, email, password_hash, otp_secret)
|
||
|
|
VALUES ($1, $2, $3, $4)
|
||
|
|
""",
|
||
|
|
username,
|
||
|
|
email,
|
||
|
|
password_hash,
|
||
|
|
otp_secret,
|
||
|
|
)
|
||
|
|
action = "opprettet"
|
||
|
|
except asyncpg.UniqueViolationError:
|
||
|
|
print("❌ Brukernavn eller e-post er allerede i bruk av en annen admin.")
|
||
|
|
sys.exit(1)
|
||
|
|
except Exception as exc:
|
||
|
|
print(f"❌ Kunne ikke bootstrappe admin-brukeren: {type(exc).__name__}")
|
||
|
|
sys.exit(1)
|
||
|
|
finally:
|
||
|
|
if conn is not None:
|
||
|
|
await conn.close()
|
||
|
|
|
||
|
|
provisioning_uri = pyotp.TOTP(otp_secret).provisioning_uri(
|
||
|
|
name=email or username,
|
||
|
|
issuer_name="TeeOff.no",
|
||
|
|
)
|
||
|
|
|
||
|
|
print("\n✅ ADMIN BRUKER KLAR")
|
||
|
|
print("-" * 50)
|
||
|
|
print(f"Brukeren '{username}' er {action}.")
|
||
|
|
print("Passordet ble satt skjult i terminalen.")
|
||
|
|
print("Legg inn denne 2FA-hemmeligheten i authenticator-appen:")
|
||
|
|
print(f"2FA-nøkkel: {otp_secret}")
|
||
|
|
print("Alternativt kan du bruke provisioning URI:")
|
||
|
|
print(provisioning_uri)
|
||
|
|
print("-" * 50 + "\n")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
try:
|
||
|
|
asyncio.run(bootstrap_admin())
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
print("\nAvbrutt.")
|
||
|
|
sys.exit(0)
|