mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
SHOW_PROFILE_EDIT_PASSWORD was replaced in 2020 by the change_password permission, settable per user or group. A post_migrate hook applied the old setting to all users so existing installs kept their behavior, and asked to remove the setting afterwards; but the settings template still set it to True. Every migrate, so every upgrade, therefore granted the permission to all users again and undid an admin's per-user or per-group choice (with False it would have revoked it from everyone). - Migration accounts.0008 applies the setting one last time, if a settings.py still has it; the post_migrate hook is gone. - New users get the permission when they are created (any path, including LDAP); the admin user form ticks it and saves the admin's choice afterwards. - The template no longer sets SHOW_PROFILE_EDIT_PASSWORD; a check (accounts.W001) asks to remove it from existing settings, and the README upgrade notes say so. The test that removes the permission and runs migrate fails on the previous code. An upgrade from master with users was simulated: the setting is applied once, and a permission removed afterwards stays removed through the next migrate.
120 lines
4.1 KiB
Python
120 lines
4.1 KiB
Python
import os
|
|
|
|
from django.apps import AppConfig
|
|
from django.db.models.signals import post_migrate
|
|
from django.db import transaction
|
|
|
|
|
|
def admin_password_path():
|
|
from django.conf import settings
|
|
|
|
return os.path.join(str(settings.BASE_DIR), "data", "admin_password")
|
|
|
|
|
|
def _migrated(state_apps, model, field=None):
|
|
"""post_migrate follows every migrate, also targeted ones and rollbacks:
|
|
act only once the post-migration state has the model (and field)."""
|
|
if state_apps is None:
|
|
return False
|
|
try:
|
|
fields = state_apps.get_model(model)._meta.get_fields()
|
|
except LookupError:
|
|
return False
|
|
return field is None or any(f.name == field for f in fields)
|
|
|
|
|
|
def grant_change_password(sender, instance, created, raw=False, **kwargs):
|
|
"""New users may change their own password unless an admin takes the
|
|
permission away; the user form saves its own choice after this."""
|
|
if not created or raw:
|
|
return
|
|
from django.contrib.auth.models import Permission
|
|
|
|
permission = Permission.objects.filter(content_type__app_label="accounts", codename="change_password").first()
|
|
if permission is not None: # created by post_migrate on a fresh database
|
|
instance.user_permissions.add(permission)
|
|
|
|
|
|
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`. Abort provisioning if private storage fails.
|
|
"""
|
|
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)
|
|
with os.fdopen(fd, "w") as f:
|
|
os.fchmod(f.fileno(), 0o600)
|
|
f.write(password + "\n")
|
|
print(f"\033[1m* \033[93mGenerated admin password written to {path}\033[0m")
|
|
except OSError as err:
|
|
raise RuntimeError(
|
|
f"Cannot securely store the generated admin password at {path}. "
|
|
"Fix the data directory permissions and run migrate again."
|
|
) from err
|
|
|
|
|
|
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
|
|
|
|
# Any real migrate run provisions while no user exists, so a run that
|
|
# failed to store the password can simply be repeated. flush (used by
|
|
# tests) sends plan=None.
|
|
if kwargs.get("plan") is None:
|
|
return
|
|
state_apps = kwargs.get("apps")
|
|
if not (_migrated(state_apps, "auth.User") and _migrated(state_apps, "accounts.UserAttributes", "must_change_password")):
|
|
return
|
|
if User.objects.exists():
|
|
return
|
|
|
|
is_testing = "test" in sys.argv
|
|
admin_user = os.environ.get("ADMIN_USERNAME", "admin")
|
|
admin_pass = os.environ.get("ADMIN_PASSWORD")
|
|
generated_password = not admin_pass and not is_testing
|
|
|
|
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")
|
|
with transaction.atomic():
|
|
admin = User.objects.create_superuser(admin_user, None, admin_pass)
|
|
UserAttributes.objects.create(
|
|
user=admin,
|
|
must_change_password=generated_password,
|
|
max_instances=-1,
|
|
max_cpus=-1,
|
|
max_memory=-1,
|
|
max_disk_size=-1,
|
|
)
|
|
|
|
|
|
class AccountsConfig(AppConfig):
|
|
name = "accounts"
|
|
verbose_name = "Accounts"
|
|
|
|
def ready(self):
|
|
post_migrate.connect(create_admin, sender=self)
|
|
from django.conf import settings
|
|
from django.db.models.signals import post_save
|
|
|
|
post_save.connect(grant_change_password, sender=settings.AUTH_USER_MODEL)
|
|
from . import checks # noqa: F401
|