Esempio n. 1
0
class User(AbstractBaseUser):
    email = models.EmailField(
        verbose_name='email address',
        max_length=191,
        unique=True,
    )
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    registration_remote_ip = models.CharField(max_length=1024, blank=True)
    locked = models.DateTimeField(null=True, blank=True)
    created = models.DateTimeField(auto_now_add=True)
    limit_domains = models.IntegerField(default=settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT, null=True, blank=True)
    dyn = models.BooleanField(default=False)

    objects = MyUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    def get_full_name(self):
        return self.email

    def get_short_name(self):
        return self.email

    def get_or_create_first_token(self):
        try:
            token = Token.objects.filter(user=self).earliest('created')
        except Token.DoesNotExist:
            token = Token.objects.create(user=self)
        return token.key

    def __str__(self):
        return self.email

    # noinspection PyMethodMayBeStatic
    def has_perm(self, *_):
        """Does the user have a specific permission?"""
        # Simplest possible answer: Yes, always
        return True

    # noinspection PyMethodMayBeStatic
    def has_module_perms(self, *_):
        """Does the user have permissions to view the app `app_label`?"""
        # Simplest possible answer: Yes, always
        return True

    @property
    def is_staff(self):
        """Is the user a member of staff?"""
        # Simplest possible answer: All admins are staff
        return self.is_admin
Esempio n. 2
0
class User(ExportModelOperationsMixin('User'), AbstractBaseUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    email = models.EmailField(
        verbose_name='email address',
        max_length=191,
        unique=True,
    )
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)
    limit_domains = models.IntegerField(
        default=settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT,
        null=True,
        blank=True)

    objects = MyUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    def get_full_name(self):
        return self.email

    def get_short_name(self):
        return self.email

    def __str__(self):
        return self.email

    # noinspection PyMethodMayBeStatic
    def has_perm(self, *_):
        """Does the user have a specific permission?"""
        # Simplest possible answer: Yes, always
        return True

    # noinspection PyMethodMayBeStatic
    def has_module_perms(self, *_):
        """Does the user have permissions to view the app `app_label`?"""
        # Simplest possible answer: Yes, always
        return True

    @property
    def is_staff(self):
        """Is the user a member of staff?"""
        # Simplest possible answer: All admins are staff
        return self.is_admin

    def activate(self):
        self.is_active = True
        self.save()

    def change_email(self, email):
        old_email = self.email
        self.email = email
        self.validate_unique()
        self.save()

        self.send_email('change-email-confirmation-old-email',
                        recipient=old_email)

    def change_password(self, raw_password):
        self.set_password(raw_password)
        self.save()
        self.send_email('password-change-confirmation')

    def send_email(self, reason, context=None, recipient=None):
        fast_lane = 'email_fast_lane'
        slow_lane = 'email_slow_lane'
        lanes = {
            'activate': slow_lane,
            'activate-with-domain': slow_lane,
            'change-email': slow_lane,
            'change-email-confirmation-old-email': fast_lane,
            'password-change-confirmation': fast_lane,
            'reset-password': fast_lane,
            'delete-user': fast_lane,
            'domain-dyndns': fast_lane,
        }
        if reason not in lanes:
            raise ValueError(
                f'Cannot send email to user {self.pk} without a good reason: {reason}'
            )

        context = context or {}
        context.setdefault(
            'link_expiration_hours',
            settings.VALIDITY_PERIOD_VERIFICATION_SIGNATURE //
            timedelta(hours=1))
        content = get_template(f'emails/{reason}/content.txt').render(context)
        content += f'\nSupport Reference: user_id = {self.pk}\n'
        footer = get_template('emails/footer.txt').render()

        logger.warning(
            f'Queuing email for user account {self.pk} (reason: {reason})')
        return EmailMessage(
            subject=get_template(f'emails/{reason}/subject.txt').render(
                context).strip(),
            body=content + footer,
            from_email=get_template('emails/from.txt').render(),
            to=[recipient or self.email],
            connection=get_connection(lane=lanes[reason],
                                      debug={
                                          'user': self.pk,
                                          'reason': reason
                                      })).send()
Esempio n. 3
0
class Domain(ExportModelOperationsMixin('Domain'), models.Model):
    class RenewalState(models.IntegerChoices):
        IMMORTAL = 0
        FRESH = 1
        NOTIFIED = 2
        WARNED = 3

    created = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=191,
                            unique=True,
                            validators=validate_domain_name)
    owner = models.ForeignKey(User,
                              on_delete=models.PROTECT,
                              related_name='domains')
    published = models.DateTimeField(null=True, blank=True)
    minimum_ttl = models.PositiveIntegerField(default=get_minimum_ttl_default)
    renewal_state = models.IntegerField(choices=RenewalState.choices,
                                        default=RenewalState.IMMORTAL)
    renewal_changed = models.DateTimeField(auto_now_add=True)
    _keys = None

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.pk is None and kwargs.get(
                'renewal_state') is None and self.is_locally_registrable:
            self.renewal_state = Domain.RenewalState.FRESH

    @cached_property
    def public_suffix(self):
        try:
            public_suffix = psl.get_public_suffix(self.name)
            is_public_suffix = psl.is_public_suffix(self.name)
        except (Timeout, NoNameservers):
            public_suffix = self.name.rpartition('.')[2]
            is_public_suffix = ('.'
                                not in self.name)  # TLDs are public suffixes
        except psl_dns.exceptions.UnsupportedRule as e:
            # It would probably be fine to treat this as a non-public suffix (with the TLD acting as the
            # public suffix and setting both public_suffix and is_public_suffix accordingly).
            # However, in order to allow to investigate the situation, it's better not catch
            # this exception. For web requests, our error handler turns it into a 503 error
            # and makes sure admins are notified.
            raise e

        if is_public_suffix:
            return public_suffix

        # Take into account that any of the parent domains could be a local public suffix. To that
        # end, identify the longest local public suffix that is actually a suffix of domain_name.
        for local_public_suffix in settings.LOCAL_PUBLIC_SUFFIXES:
            has_local_public_suffix_parent = (
                '.' + self.name).endswith('.' + local_public_suffix)
            if has_local_public_suffix_parent and len(
                    local_public_suffix) > len(public_suffix):
                public_suffix = local_public_suffix

        return public_suffix

    def is_covered_by_foreign_zone(self):
        # Generate a list of all domains connecting this one and its public suffix.
        # If another user owns a zone with one of these names, then the requested
        # domain is unavailable because it is part of the other user's zone.
        private_components = self.name.rsplit(self.public_suffix,
                                              1)[0].rstrip('.')
        private_components = private_components.split(
            '.') if private_components else []
        private_domains = [
            '.'.join(private_components[i:])
            for i in range(0, len(private_components))
        ]
        private_domains = [
            f'{private_domain}.{self.public_suffix}'
            for private_domain in private_domains
        ]
        assert self.name == next(iter(private_domains), self.public_suffix)

        # Determine whether domain is covered by other users' zones
        return Domain.objects.filter(
            Q(name__in=private_domains)
            & ~Q(owner=self._owner_or_none)).exists()

    def covers_foreign_zone(self):
        # Note: This is not completely accurate: Ideally, we should only consider zones with identical public suffix.
        # (If a public suffix lies in between, it's ok.) However, as there could be many descendant zones, the accurate
        # check is expensive, so currently not implemented (PSL lookups for each of them).
        return Domain.objects.filter(
            Q(name__endswith=f'.{self.name}')
            & ~Q(owner=self._owner_or_none)).exists()

    def is_registrable(self):
        """
        Returns False if the domain name is reserved, a public suffix, or covered by / covers another user's domain.
        Otherwise, True is returned.
        """
        self.clean()  # ensure .name is a domain name
        private_generation = self.name.count('.') - self.public_suffix.count(
            '.')
        assert private_generation >= 0

        # .internal is reserved
        if f'.{self.name}'.endswith('.internal'):
            return False

        # Public suffixes can only be registered if they are local
        if private_generation == 0 and self.name not in settings.LOCAL_PUBLIC_SUFFIXES:
            return False

        # Disallow _acme-challenge.dedyn.io and the like. Rejects reserved direct children of public suffixes.
        reserved_prefixes = (
            '_',
            'autoconfig.',
            'autodiscover.',
        )
        if private_generation == 1 and any(
                self.name.startswith(prefix) for prefix in reserved_prefixes):
            return False

        # Domains covered by another user's zone can't be registered
        if self.is_covered_by_foreign_zone():
            return False

        # Domains that would cover another user's zone can't be registered
        if self.covers_foreign_zone():
            return False

        return True

    @property
    def keys(self):
        if not self._keys:
            self._keys = pdns.get_keys(self)
        return self._keys

    @property
    def touched(self):
        try:
            rrset_touched = max(updated
                                for updated in self.rrset_set.values_list(
                                    'touched', flat=True))
            # If the domain has not been published yet, self.published is None and max() would fail
            return rrset_touched if not self.published else max(
                rrset_touched, self.published)
        except ValueError:
            # This can be none if the domain was never published and has no records (but there should be at least NS)
            return self.published

    @property
    def is_locally_registrable(self):
        return self.parent_domain_name in settings.LOCAL_PUBLIC_SUFFIXES

    @property
    def _owner_or_none(self):
        try:
            return self.owner
        except Domain.owner.RelatedObjectDoesNotExist:
            return None

    @property
    def parent_domain_name(self):
        return self._partitioned_name[1]

    @property
    def _partitioned_name(self):
        subname, _, parent_name = self.name.partition('.')
        return subname, parent_name or None

    def save(self, *args, **kwargs):
        self.full_clean(validate_unique=False)
        super().save(*args, **kwargs)

    def update_delegation(self, child_domain: Domain):
        child_subname, child_domain_name = child_domain._partitioned_name
        if self.name != child_domain_name:
            raise ValueError(
                'Cannot update delegation of %s as it is not an immediate child domain of %s.'
                % (child_domain.name, self.name))

        if child_domain.pk:
            # Domain real: set delegation
            child_keys = child_domain.keys
            if not child_keys:
                raise APIException(
                    'Cannot delegate %s, as it currently has no keys.' %
                    child_domain.name)

            RRset.objects.create(domain=self,
                                 subname=child_subname,
                                 type='NS',
                                 ttl=3600,
                                 contents=settings.DEFAULT_NS)
            RRset.objects.create(
                domain=self,
                subname=child_subname,
                type='DS',
                ttl=300,
                contents=[ds for k in child_keys for ds in k['ds']])
            metrics.get('desecapi_autodelegation_created').inc()
        else:
            # Domain not real: remove delegation
            for rrset in self.rrset_set.filter(subname=child_subname,
                                               type__in=['NS', 'DS']):
                rrset.delete()
            metrics.get('desecapi_autodelegation_deleted').inc()

    def delete(self):
        ret = super().delete()
        logger.warning(f'Domain {self.name} deleted (owner: {self.owner.pk})')
        return ret

    def __str__(self):
        return self.name

    class Meta:
        ordering = ('created', )