from django.conf import settings from django.core.exceptions import ValidationError from django.core.management.base import BaseCommand, CommandError from django.db import IntegrityError, transaction from members.models import Account class Command(BaseCommand): help = "Explicitly bind an unlinked local account to an independently verified Dex subject." def add_arguments(self, parser): parser.add_argument("--account", required=True, help="Local account UUID, not an email address.") parser.add_argument("--uid", required=True, help="Independently verified LDAP connector uid.") parser.add_argument("--subject", required=True, help="Exact verified Dex sub for the configured issuer/connector.") def handle(self, *args, **options): subject = options["subject"] if not subject or len(subject) > 512: raise CommandError("A nonempty Dex subject of at most 512 characters is required.") try: with transaction.atomic(): account = Account.objects.select_for_update().get(pk=options["account"]) if account.uid != options["uid"]: raise CommandError("The verified uid does not match the selected account.") if account.oidc_sub: if account.oidc_sub == subject and account.oidc_issuer == settings.DEX_ISSUER: self.stdout.write("Dex identity already bound.") return raise CommandError("Account already has a Dex binding; automatic rebinding is forbidden.") account.oidc_issuer = settings.DEX_ISSUER account.oidc_sub = subject account.save(update_fields=["oidc_issuer", "oidc_sub"]) except (Account.DoesNotExist, ValidationError, ValueError) as exc: raise CommandError("Select an existing account by UUID.") from exc except IntegrityError: raise CommandError("Dex subject is already bound to another account.") from None self.stdout.write(self.style.SUCCESS("Dex identity bound. No LDAP or billing data changed."))