1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00
webvirtcloud/accounts/apps.py
catborise 78852eb4c8 feat(accounts): force a password change at the first login with the generated password
While data/admin_password exists and a user's password still matches it,
the login sets a session flag (checked once at login, not per request).
ForcePasswordChangeMiddleware then sends that user to the password-change
page for every view except change password, login and logout. Setting a
new password clears the flag and deletes data/admin_password. No schema
change; if ADMIN_PASSWORD was set, there is no file and nothing is forced.
2026-10-02 23:55:31 +03:00

124 lines
4.4 KiB
Python

import os
from django.apps import AppConfig
from django.contrib.auth.signals import user_logged_in
from django.db.models.signals import post_migrate
def admin_password_path():
from django.conf import settings
return os.path.join(str(settings.BASE_DIR), "data", "admin_password")
def flag_generated_password(sender, request, user, **kwargs):
"""
While a user still has the generated admin password, make them set a new
one before using the panel (ROADMAP O-17; see ForcePasswordChangeMiddleware).
"""
try:
with open(admin_password_path()) as f:
generated = f.read().strip()
except OSError:
return
if generated and user.check_password(generated):
request.session["must_change_password"] = True
def apply_change_password(sender, **kwargs):
"""
Apply new change_password permission for all users
Depending on settings SHOW_PROFILE_EDIT_PASSWORD
"""
from django.conf import settings
from django.contrib.auth.models import Permission, User
if hasattr(settings, "SHOW_PROFILE_EDIT_PASSWORD"):
print("\033[1m! \033[92mSHOW_PROFILE_EDIT_PASSWORD is found inside settings.py\033[0m")
print("\033[1m* \033[92mApplying permission can_change_password for all users\033[0m")
users = User.objects.all()
permission = Permission.objects.get(codename="change_password")
if settings.SHOW_PROFILE_EDIT_PASSWORD:
print("\033[1m! \033[91mWarning!!! Setting to True for all users\033[0m")
for user in users:
user.user_permissions.add(permission)
else:
print("\033[1m* \033[91mWarning!!! Setting to False for all users\033[0m")
for user in users:
user.user_permissions.remove(permission)
print("\033[1m! Don`t forget to remove the option from settings.py\033[0m")
def _store_generated_password(password):
"""
Write the generated admin password to data/admin_password (mode 0600)
instead of printing it, so it does not end up in install logs or
`docker logs` (ROADMAP O-02). Falls back to printing if the file cannot
be written, so the admin is never locked out.
"""
path = admin_password_path()
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
# Never write through a pre-existing file or symlink (this runs as
# root in Docker, while data/ belongs to www-data).
if os.path.lexists(path):
os.unlink(path)
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
os.fchmod(fd, 0o600)
with os.fdopen(fd, "w") as f:
f.write(password + "\n")
print(f"\033[1m* \033[93mGenerated admin password written to {path}\033[0m")
except OSError:
print(f"\033[1m* \033[93mGenerated random admin password: {password}\033[0m")
def create_admin(sender, **kwargs):
"""
Create initial admin user
"""
import os
import secrets
import sys
from django.contrib.auth.models import User
from accounts.models import UserAttributes
plan = kwargs.get("plan", [])
for migration, rolled_back in plan:
if (
migration.app_label == "accounts"
and migration.name == "0001_initial"
and not rolled_back
):
if User.objects.count() == 0:
is_testing = "test" in sys.argv
admin_user = os.environ.get("ADMIN_USERNAME", "admin")
admin_pass = os.environ.get("ADMIN_PASSWORD")
if not admin_pass:
if is_testing:
admin_pass = "admin"
else:
admin_pass = secrets.token_urlsafe(16)
_store_generated_password(admin_pass)
print(f"\033[1m* \033[92mCreating default admin user '{admin_user}'\033[0m")
admin = User.objects.create_superuser(admin_user, None, admin_pass)
UserAttributes(
user=admin,
max_instances=-1,
max_cpus=-1,
max_memory=-1,
max_disk_size=-1,
).save()
break
class AccountsConfig(AppConfig):
name = "accounts"
verbose_name = "Accounts"
def ready(self):
post_migrate.connect(create_admin, sender=self)
post_migrate.connect(apply_change_password, sender=self)
user_logged_in.connect(flag_generated_password)