Exemple #1
0
class Token(ExportModelOperationsMixin('Token'),
            rest_framework.authtoken.models.Token):
    @staticmethod
    def _allowed_subnets_default():
        return [
            ipaddress.IPv4Network('0.0.0.0/0'),
            ipaddress.IPv6Network('::/0')
        ]

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    key = models.CharField("Key", max_length=128, db_index=True, unique=True)
    user = models.ForeignKey(User,
                             related_name='auth_tokens',
                             on_delete=models.CASCADE,
                             verbose_name="User")
    name = models.CharField('Name', blank=True, max_length=64)
    last_used = models.DateTimeField(null=True, blank=True)
    perm_manage_tokens = models.BooleanField(default=False)
    allowed_subnets = ArrayField(CidrAddressField(),
                                 default=_allowed_subnets_default.__func__)
    max_age = models.DurationField(
        null=True, default=None, validators=[MinValueValidator(timedelta(0))])
    max_unused_period = models.DurationField(
        null=True, default=None, validators=[MinValueValidator(timedelta(0))])

    plain = None
    objects = NetManager()

    @property
    def is_valid(self):
        now = timezone.now()

        # Check max age
        try:
            if self.created + self.max_age < now:
                return False
        except TypeError:
            pass

        # Check regular usage requirement
        try:
            if (self.last_used or self.created) + self.max_unused_period < now:
                return False
        except TypeError:
            pass

        return True

    def generate_key(self):
        self.plain = secrets.token_urlsafe(21)
        self.key = Token.make_hash(self.plain)
        return self.key

    @staticmethod
    def make_hash(plain):
        return make_password(plain,
                             salt='static',
                             hasher='pbkdf2_sha256_iter1')
Exemple #2
0
class RRset(ExportModelOperationsMixin('RRset'), models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(
        null=True)  # undocumented, used for debugging only
    domain = models.ForeignKey(Domain, on_delete=models.CASCADE)
    subname = models.CharField(
        max_length=178,
        blank=True,
        validators=[
            validate_lower,
            RegexValidator(
                regex=r'^([*]|(([*][.])?[a-z0-9_.-]*))$',
                message=
                'Subname can only use (lowercase) a-z, 0-9, ., -, and _, '
                'may start with a \'*.\', or just be \'*\'.',
                code='invalid_subname')
        ])
    type = models.CharField(
        max_length=10,
        validators=[
            validate_upper,
            RegexValidator(
                regex=r'^[A-Z][A-Z0-9]*$',
                message=
                'Type must be uppercase alphanumeric and start with a letter.',
                code='invalid_type')
        ])
    ttl = models.PositiveIntegerField()

    objects = RRsetManager()

    DEAD_TYPES = ('ALIAS', 'DNAME')
    RESTRICTED_TYPES = ('SOA', 'RRSIG', 'DNSKEY', 'NSEC3PARAM', 'OPT')

    class Meta:
        unique_together = (("domain", "subname", "type"), )

    @staticmethod
    def construct_name(subname, domain_name):
        return '.'.join(filter(None, [subname, domain_name])) + '.'

    @property
    def name(self):
        return self.construct_name(self.subname, self.domain.name)

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

    def __str__(self):
        return '<RRSet %s domain=%s type=%s subname=%s>' % (
            self.pk, self.domain.name, self.type, self.subname)
Exemple #3
0
class Captcha(ExportModelOperationsMixin('Captcha'), models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    created = models.DateTimeField(auto_now_add=True)
    content = models.CharField(
        max_length=24,
        default=captcha_default_content,
    )

    def verify(self, solution: str):
        age = timezone.now() - self.created
        self.delete()
        return (str(solution).upper().strip()
                == self.content  # solution correct
                and age <= settings.CAPTCHA_VALIDITY_PERIOD  # not expired
                )
Exemple #4
0
class Token(ExportModelOperationsMixin('Token'),
            rest_framework.authtoken.models.Token):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    key = models.CharField("Key", max_length=128, db_index=True, unique=True)
    user = models.ForeignKey(User,
                             related_name='auth_tokens',
                             on_delete=models.CASCADE,
                             verbose_name="User")
    name = models.CharField("Name", max_length=64, default="")
    plain = None

    def generate_key(self):
        self.plain = urlsafe_b64encode(urandom(21)).decode()
        self.key = Token.make_hash(self.plain)
        return self.key

    @staticmethod
    def make_hash(plain):
        return make_password(plain,
                             salt='static',
                             hasher='pbkdf2_sha256_iter1')
Exemple #5
0
class Token(ExportModelOperationsMixin('Token'),
            rest_framework.authtoken.models.Token):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    key = models.CharField("Key", max_length=128, db_index=True, unique=True)
    user = models.ForeignKey(User,
                             related_name='auth_tokens',
                             on_delete=models.CASCADE,
                             verbose_name="User")
    name = models.CharField('Name', blank=True, max_length=64)
    last_used = models.DateTimeField(null=True, blank=True)
    perm_manage_tokens = models.BooleanField(default=False)

    plain = None

    def generate_key(self):
        self.plain = secrets.token_urlsafe(21)
        self.key = Token.make_hash(self.plain)
        return self.key

    @staticmethod
    def make_hash(plain):
        return make_password(plain,
                             salt='static',
                             hasher='pbkdf2_sha256_iter1')
Exemple #6
0
class Captcha(ExportModelOperationsMixin('Captcha'), models.Model):
    class Kind(models.TextChoices):
        IMAGE = 'image'
        AUDIO = 'audio'

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    created = models.DateTimeField(auto_now_add=True)
    content = models.CharField(max_length=24, default="")
    kind = models.CharField(choices=Kind.choices,
                            default=Kind.IMAGE,
                            max_length=24)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not self.content:
            self.content = captcha_default_content(self.kind)

    def verify(self, solution: str):
        age = timezone.now() - self.created
        self.delete()
        return (str(solution).upper().strip()
                == self.content  # solution correct
                and age <= settings.CAPTCHA_VALIDITY_PERIOD  # not expired
                )
Exemple #7
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()
Exemple #8
0
class RRset(ExportModelOperationsMixin('RRset'), models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    created = models.DateTimeField(auto_now_add=True)
    touched = models.DateTimeField(auto_now=True)
    domain = models.ForeignKey(Domain, on_delete=models.CASCADE)
    subname = models.CharField(
        max_length=178,
        blank=True,
        validators=[
            validate_lower,
            RegexValidator(
                regex=r'^([*]|(([*][.])?([a-z0-9_-]+[.])*[a-z0-9_-]+))$',
                message=
                'Subname can only use (lowercase) a-z, 0-9, ., -, and _, '
                'may start with a \'*.\', or just be \'*\'.',
                code='invalid_subname')
        ])
    type = models.CharField(
        max_length=10,
        validators=[
            validate_upper,
            RegexValidator(
                regex=r'^[A-Z][A-Z0-9]*$',
                message=
                'Type must be uppercase alphanumeric and start with a letter.',
                code='invalid_type')
        ])
    ttl = models.PositiveIntegerField()

    objects = RRsetManager()

    class Meta:
        unique_together = (("domain", "subname", "type"), )

    @staticmethod
    def construct_name(subname, domain_name):
        return '.'.join(filter(None, [subname, domain_name])) + '.'

    @property
    def name(self):
        return self.construct_name(self.subname, self.domain.name)

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

    def clean_records(self, records_presentation_format):
        """
        Validates the records belonging to this set. Validation rules follow the DNS specification; some types may
        incur additional validation rules.

        Raises ValidationError if violation of DNS specification is found.

        Returns a set of records in canonical presentation format.

        :param records_presentation_format: iterable of records in presentation format
        """
        rdtype = rdatatype.from_text(self.type)
        errors = []

        def _error_msg(record, detail):
            return f'Record content of {self.type} {self.name} invalid: \'{record}\': {detail}'

        records_canonical_format = set()
        for r in records_presentation_format:
            try:
                r_canonical_format = RR.canonical_presentation_format(
                    r, rdtype)
            except binascii.Error:
                # e.g., odd-length string
                errors.append(
                    _error_msg(
                        r,
                        'Cannot parse hexadecimal or base64 record contents'))
            except dns.exception.SyntaxError as e:
                # e.g., A/127.0.0.999
                if 'quote' in e.args[0]:
                    errors.append(
                        _error_msg(
                            r,
                            f'Data for {self.type} records must be given using quotation marks.'
                        ))
                else:
                    errors.append(
                        _error_msg(
                            r,
                            f'Record content malformed: {",".join(e.args)}'))
            except dns.name.NeedAbsoluteNameOrOrigin:
                errors.append(
                    _error_msg(
                        r,
                        'Hostname must be fully qualified (i.e., end in a dot: "example.com.")'
                    ))
            except ValueError:
                # e.g., string ("asdf") cannot be parsed into int on base 10
                errors.append(_error_msg(r, 'Cannot parse record contents'))
            except Exception as e:
                # TODO see what exceptions raise here for faulty input
                raise e
            else:
                if r_canonical_format in records_canonical_format:
                    errors.append(
                        _error_msg(
                            r,
                            f'Duplicate record content: this is identical to '
                            f'\'{r_canonical_format}\''))
                else:
                    records_canonical_format.add(r_canonical_format)

        if any(errors):
            raise ValidationError(errors)

        return records_canonical_format

    def save_records(self, records):
        """
        Updates this RR set's resource records, discarding any old values.

        Records are expected in presentation format and are converted to canonical
        presentation format (e.g., 127.00.0.1 will be converted to 127.0.0.1).
        Raises if a invalid set of records is provided.

        This method triggers the following database queries:
        - one DELETE query
        - one SELECT query for comparison of old with new records
        - one INSERT query, if one or more records were added

        Changes are saved to the database immediately.

        :param records: list of records in presentation format
        """
        new_records = self.clean_records(records)

        # Delete RRs that are not in the new record list from the DB
        self.records.exclude(content__in=new_records).delete()  # one DELETE

        # Retrieve all remaining RRs from the DB
        unchanged_records = set(r.content
                                for r in self.records.all())  # one SELECT

        # Save missing RRs from the new record list to the DB
        added_records = new_records - unchanged_records
        rrs = [RR(rrset=self, content=content) for content in added_records]
        RR.objects.bulk_create(rrs)  # One INSERT

    def __str__(self):
        return '<RRSet %s domain=%s type=%s subname=%s>' % (
            self.pk, self.domain.name, self.type, self.subname)
Exemple #9
0
class RRset(ExportModelOperationsMixin('RRset'), models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    created = models.DateTimeField(auto_now_add=True)
    touched = models.DateTimeField(auto_now=True, db_index=True)
    domain = models.ForeignKey(Domain, on_delete=models.CASCADE)
    subname = models.CharField(
        max_length=178,
        blank=True,
        validators=[
            validate_lower,
            RegexValidator(
                regex=
                r'^([*]|(([*][.])?([a-z0-9_-]{1,63}[.])*[a-z0-9_-]{1,63}))$',
                message=
                'Subname can only use (lowercase) a-z, 0-9, ., -, and _, '
                'may start with a \'*.\', or just be \'*\'. Components may not exceed 63 characters.',
                code='invalid_subname')
        ])
    type = models.CharField(
        max_length=10,
        validators=[
            validate_upper,
            RegexValidator(
                regex=r'^[A-Z][A-Z0-9]*$',
                message=
                'Type must be uppercase alphanumeric and start with a letter.',
                code='invalid_type')
        ])
    ttl = models.PositiveIntegerField()

    objects = RRsetManager()

    class Meta:
        constraints = [
            ExclusionConstraint(
                name='cname_exclusivity',
                expressions=[
                    ('domain', RangeOperators.EQUAL),
                    ('subname', RangeOperators.EQUAL),
                    (RawSQL("int4(type = 'CNAME')",
                            ()), RangeOperators.NOT_EQUAL),
                ],
            ),
        ]
        unique_together = (("domain", "subname", "type"), )

    @staticmethod
    def construct_name(subname, domain_name):
        return '.'.join(filter(None, [subname, domain_name])) + '.'

    @property
    def name(self):
        return self.construct_name(self.subname, self.domain.name)

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

    def clean_records(self, records_presentation_format):
        """
        Validates the records belonging to this set. Validation rules follow the DNS specification; some types may
        incur additional validation rules.

        Raises ValidationError if violation of DNS specification is found.

        Returns a set of records in canonical presentation format.

        :param records_presentation_format: iterable of records in presentation format
        """
        errors = []

        if self.type == 'CNAME':
            if self.subname == '':
                errors.append('CNAME RRset cannot have empty subname.')
            if len(records_presentation_format) > 1:
                errors.append('CNAME RRset cannot have multiple records.')

        def _error_msg(record, detail):
            return f'Record content of {self.type} {self.name} invalid: \'{record}\': {detail}'

        records_canonical_format = set()
        for r in records_presentation_format:
            try:
                r_canonical_format = RR.canonical_presentation_format(
                    r, self.type)
            except ValueError as ex:
                errors.append(_error_msg(r, str(ex)))
            else:
                if r_canonical_format in records_canonical_format:
                    errors.append(
                        _error_msg(
                            r,
                            f'Duplicate record content: this is identical to '
                            f'\'{r_canonical_format}\''))
                else:
                    records_canonical_format.add(r_canonical_format)

        if any(errors):
            raise ValidationError(errors)

        return records_canonical_format

    def save_records(self, records):
        """
        Updates this RR set's resource records, discarding any old values.

        Records are expected in presentation format and are converted to canonical
        presentation format (e.g., 127.00.0.1 will be converted to 127.0.0.1).
        Raises if a invalid set of records is provided.

        This method triggers the following database queries:
        - one DELETE query
        - one SELECT query for comparison of old with new records
        - one INSERT query, if one or more records were added

        Changes are saved to the database immediately.

        :param records: list of records in presentation format
        """
        new_records = self.clean_records(records)

        # Delete RRs that are not in the new record list from the DB
        self.records.exclude(content__in=new_records).delete()  # one DELETE

        # Retrieve all remaining RRs from the DB
        unchanged_records = set(r.content
                                for r in self.records.all())  # one SELECT

        # Save missing RRs from the new record list to the DB
        added_records = new_records - unchanged_records
        rrs = [RR(rrset=self, content=content) for content in added_records]
        RR.objects.bulk_create(rrs)  # One INSERT

    def __str__(self):
        return '<RRSet %s domain=%s type=%s subname=%s>' % (
            self.pk, self.domain.name, self.type, self.subname)