mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 02:19:23 +02:00
Second revision of Domain model draft
This commit is contained in:
parent
523c699865
commit
2b91a3c1d1
12 changed files with 561 additions and 33 deletions
|
@ -11,7 +11,7 @@ import requests
|
||||||
|
|
||||||
from cachetools.func import ttl_cache
|
from cachetools.func import ttl_cache
|
||||||
|
|
||||||
from registrar.models import Website
|
from registrar.models import Domain
|
||||||
|
|
||||||
DOMAIN_FILE_URL = (
|
DOMAIN_FILE_URL = (
|
||||||
"https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv"
|
"https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv"
|
||||||
|
@ -35,7 +35,7 @@ def _domains():
|
||||||
# get the domain before the first comma
|
# get the domain before the first comma
|
||||||
domain = line.split(",", 1)[0]
|
domain = line.split(",", 1)[0]
|
||||||
# sanity-check the string we got from the file here
|
# sanity-check the string we got from the file here
|
||||||
if Website.string_could_be_domain(domain):
|
if Domain.string_could_be_domain(domain):
|
||||||
# lowercase everything when we put it in domains
|
# lowercase everything when we put it in domains
|
||||||
domains.add(domain.lower())
|
domains.add(domain.lower())
|
||||||
return domains
|
return domains
|
||||||
|
@ -68,8 +68,8 @@ def available(request, domain=""):
|
||||||
# validate that the given domain could be a domain name and fail early if
|
# validate that the given domain could be a domain name and fail early if
|
||||||
# not.
|
# not.
|
||||||
if not (
|
if not (
|
||||||
Website.string_could_be_domain(domain)
|
Domain.string_could_be_domain(domain)
|
||||||
or Website.string_could_be_domain(domain + ".gov")
|
or Domain.string_could_be_domain(domain + ".gov")
|
||||||
):
|
):
|
||||||
raise BadRequest("Invalid request.")
|
raise BadRequest("Invalid request.")
|
||||||
# a domain is available if it is NOT in the list of current domains
|
# a domain is available if it is NOT in the list of current domains
|
||||||
|
|
0
src/epp/__init__.py
Normal file
0
src/epp/__init__.py
Normal file
40
src/epp/mock_epp.py
Normal file
40
src/epp/mock_epp.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"""
|
||||||
|
This file defines a number of mock functions which can be used to simulate
|
||||||
|
communication with the registry until that integration is implemented.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def domain_check(_):
|
||||||
|
""" Is domain available for registration? """
|
||||||
|
return True
|
||||||
|
|
||||||
|
def domain_info(domain):
|
||||||
|
""" What does the registry know about this domain? """
|
||||||
|
return {
|
||||||
|
"name": domain,
|
||||||
|
"roid": "EXAMPLE1-REP",
|
||||||
|
"status": ["ok"],
|
||||||
|
"registrant": "jd1234",
|
||||||
|
"contact": {
|
||||||
|
"admin": "sh8013",
|
||||||
|
"tech": None,
|
||||||
|
},
|
||||||
|
"ns": {
|
||||||
|
f"ns1.{domain}",
|
||||||
|
f"ns2.{domain}",
|
||||||
|
},
|
||||||
|
"host": [
|
||||||
|
f"ns1.{domain}",
|
||||||
|
f"ns2.{domain}",
|
||||||
|
],
|
||||||
|
"sponsor": "ClientX",
|
||||||
|
"creator": "ClientY",
|
||||||
|
# TODO: think about timezones
|
||||||
|
"creation_date": datetime.today(),
|
||||||
|
"updator": "ClientX",
|
||||||
|
"last_update_date": datetime.today(),
|
||||||
|
"expiration_date": datetime.today(),
|
||||||
|
"last_transfer_date": datetime.today(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-17 13:46
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_fsm
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Domain",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
default=None,
|
||||||
|
help_text="Fully qualified domain name",
|
||||||
|
max_length=253,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
django_fsm.FSMField(
|
||||||
|
choices=[(True, "Yes"), (False, "No")],
|
||||||
|
default=False,
|
||||||
|
help_text="Domain is live in the registry",
|
||||||
|
max_length=50,
|
||||||
|
protected=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("owners", models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Host",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
default=None,
|
||||||
|
help_text="Fully qualified domain name",
|
||||||
|
max_length=253,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"domain",
|
||||||
|
models.ForeignKey(
|
||||||
|
help_text="Domain to which this host belongs",
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="registrar.domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Nameserver",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"host_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="registrar.host",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=("registrar.host",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="HostIP",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"address",
|
||||||
|
models.CharField(
|
||||||
|
default=None,
|
||||||
|
help_text="IP address",
|
||||||
|
max_length=46,
|
||||||
|
validators=[django.core.validators.validate_ipv46_address],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"host",
|
||||||
|
models.ForeignKey(
|
||||||
|
help_text="Host to which this IP address belongs",
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="registrar.host",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="requested_domain",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The requested domain",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="domain_application",
|
||||||
|
to="registrar.domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="domain",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("is_active", True)),
|
||||||
|
fields=("name",),
|
||||||
|
name="unique_domain_name_in_registry",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,6 +2,10 @@ from auditlog.registry import auditlog # type: ignore
|
||||||
|
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
from .domain_application import DomainApplication
|
from .domain_application import DomainApplication
|
||||||
|
from .domain import Domain
|
||||||
|
from .host_ip import HostIP
|
||||||
|
from .host import Host
|
||||||
|
from .nameserver import Nameserver
|
||||||
from .user_profile import UserProfile
|
from .user_profile import UserProfile
|
||||||
from .user import User
|
from .user import User
|
||||||
from .website import Website
|
from .website import Website
|
||||||
|
@ -9,6 +13,10 @@ from .website import Website
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Contact",
|
"Contact",
|
||||||
"DomainApplication",
|
"DomainApplication",
|
||||||
|
"Domain",
|
||||||
|
"HostIP",
|
||||||
|
"Host",
|
||||||
|
"Nameserver",
|
||||||
"UserProfile",
|
"UserProfile",
|
||||||
"User",
|
"User",
|
||||||
"Website",
|
"Website",
|
||||||
|
@ -16,6 +24,10 @@ __all__ = [
|
||||||
|
|
||||||
auditlog.register(Contact)
|
auditlog.register(Contact)
|
||||||
auditlog.register(DomainApplication)
|
auditlog.register(DomainApplication)
|
||||||
|
auditlog.register(Domain)
|
||||||
|
auditlog.register(HostIP)
|
||||||
|
auditlog.register(Host)
|
||||||
|
auditlog.register(Nameserver)
|
||||||
auditlog.register(UserProfile)
|
auditlog.register(UserProfile)
|
||||||
auditlog.register(User)
|
auditlog.register(User)
|
||||||
auditlog.register(Website)
|
auditlog.register(Website)
|
||||||
|
|
221
src/registrar/models/domain.py
Normal file
221
src/registrar/models/domain.py
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django_fsm import FSMField, transition # type: ignore
|
||||||
|
|
||||||
|
from epp.mock_epp import domain_info, domain_check
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
from .domain_application import DomainApplication
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Domain(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
Manage the lifecycle of domain names.
|
||||||
|
|
||||||
|
The registry is the source of truth for this data and this model exists:
|
||||||
|
1. To tie ownership information in the registrar to
|
||||||
|
DNS entries in the registry; and
|
||||||
|
2. To allow a new registrant to draft DNS entries before their
|
||||||
|
application is approved
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
# draft domains may share the same name, but
|
||||||
|
# once approved, they must be globally unique
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=['name'],
|
||||||
|
condition=models.Q(is_active=True),
|
||||||
|
name='unique_domain_name_in_registry'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Status(models.TextChoices):
|
||||||
|
"""The status codes we can receive from the registry."""
|
||||||
|
# Requests to delete the object MUST be rejected.
|
||||||
|
CLIENT_DELETE_PROHIBITED = "clientDeleteProhibited"
|
||||||
|
SERVER_DELETE_PROHIBITED = "serverDeleteProhibited"
|
||||||
|
|
||||||
|
# DNS delegation information MUST NOT be published for the object.
|
||||||
|
CLIENT_HOLD = "clientHold"
|
||||||
|
SERVER_HOLD = "serverHold"
|
||||||
|
|
||||||
|
# Requests to renew the object MUST be rejected.
|
||||||
|
CLIENT_RENEW_PROHIBITED = "clientRenewProhibited"
|
||||||
|
SERVER_RENEW_PROHIBITED = "serverRenewProhibited"
|
||||||
|
|
||||||
|
# Requests to transfer the object MUST be rejected.
|
||||||
|
CLIENT_TRANSFER_PROHIBITED = "clientTransferProhibited"
|
||||||
|
SERVER_TRANSFER_PROHIBITED = "serverTransferProhibited"
|
||||||
|
|
||||||
|
# Requests to update the object (other than to remove this status)
|
||||||
|
# MUST be rejected.
|
||||||
|
CLIENT_UPDATE_PROHIBITED = "clientUpdateProhibited"
|
||||||
|
SERVER_UPDATE_PROHIBITED = "serverUpdateProhibited"
|
||||||
|
|
||||||
|
# Delegation information has not been associated with the object.
|
||||||
|
# This is the default status when a domain object is first created
|
||||||
|
# and there are no associated host objects for the DNS delegation.
|
||||||
|
# This status can also be set by the server when all host-object
|
||||||
|
# associations are removed.
|
||||||
|
INACTIVE = "inactive"
|
||||||
|
|
||||||
|
# This is the normal status value for an object that has no pending
|
||||||
|
# operations or prohibitions. This value is set and removed by the
|
||||||
|
# server as other status values are added or removed.
|
||||||
|
OK = "ok"
|
||||||
|
|
||||||
|
# A transform command has been processed for the object, but the
|
||||||
|
# action has not been completed by the server. Server operators can
|
||||||
|
# delay action completion for a variety of reasons, such as to allow
|
||||||
|
# for human review or third-party action. A transform command that
|
||||||
|
# is processed, but whose requested action is pending, is noted with
|
||||||
|
# response code 1001.
|
||||||
|
PENDING_CREATE = "pendingCreate"
|
||||||
|
PENDING_DELETE = "pendingDelete"
|
||||||
|
PENDING_RENEW = "pendingRenew"
|
||||||
|
PENDING_TRANSFER = "pendingTransfer"
|
||||||
|
PENDING_UPDATE = "pendingUpdate"
|
||||||
|
|
||||||
|
|
||||||
|
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
||||||
|
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
||||||
|
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def string_could_be_domain(cls, domain: str) -> bool:
|
||||||
|
"""Return True if the string could be a domain name, otherwise False."""
|
||||||
|
if cls.DOMAIN_REGEX.match(domain):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def available(cls, domain: str) -> bool:
|
||||||
|
"""Check if a domain is available. Not implemented. """
|
||||||
|
return domain_check(domain)
|
||||||
|
|
||||||
|
def transfer(self):
|
||||||
|
""" Going somewhere. Not implemented. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def renew(self):
|
||||||
|
""" Time to renew. Not implemented. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_property(self, property):
|
||||||
|
"""Get some info about a domain."""
|
||||||
|
if not self.is_active: return None
|
||||||
|
if not hasattr(self, "info"):
|
||||||
|
try:
|
||||||
|
# get info from registry
|
||||||
|
self.info = domain_info(self.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
# TODO: back off error handling
|
||||||
|
return None
|
||||||
|
if hasattr(self, "info") and (property in self.info):
|
||||||
|
return self.info[property]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def could_be_domain(self) -> bool:
|
||||||
|
"""Could this instance be a domain?"""
|
||||||
|
# short-circuit if self.website is null/None
|
||||||
|
if not self.name:
|
||||||
|
return False
|
||||||
|
return self.string_could_be_domain(str(self.name))
|
||||||
|
|
||||||
|
@transition(field="is_active", source="*", target=True)
|
||||||
|
def activate(self):
|
||||||
|
"""This domain should be made live."""
|
||||||
|
if hasattr(self, "domain_application"):
|
||||||
|
if self.domain_application.status != DomainApplication.APPROVED:
|
||||||
|
raise ValueError("Cannot activate. Application must be approved.")
|
||||||
|
if Domain.objects.filter(name=self.name, is_active=True).exists():
|
||||||
|
raise ValueError("Cannot activate. Domain name is already in use.")
|
||||||
|
# TODO: depending on the details of our registry integration
|
||||||
|
# we will either contact the registry and deploy the domain
|
||||||
|
# in this function OR we will verify that it has already been
|
||||||
|
# activated and reject this state transition if it has not
|
||||||
|
pass
|
||||||
|
|
||||||
|
@transition(field="is_active", source="*", target=False)
|
||||||
|
def deactivate(self):
|
||||||
|
"""This domain should not be live."""
|
||||||
|
# there are security concerns to having this function exist
|
||||||
|
# within the codebase; discuss these with the project lead
|
||||||
|
# if there is a feature request to implement this
|
||||||
|
raise Exception("Cannot revoke, contact registry.")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def roid(self):
|
||||||
|
return self._get_property("roid")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
return self._get_property("status")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def registrant(self):
|
||||||
|
return self._get_property("registrant")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sponsor(self):
|
||||||
|
return self._get_property("sponsor")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def creator(self):
|
||||||
|
return self._get_property("creator")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def creation_date(self):
|
||||||
|
return self._get_property("creation_date")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updator(self):
|
||||||
|
return self._get_property("updator")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_update_date(self):
|
||||||
|
return self._get_property("last_update_date")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expiration_date(self):
|
||||||
|
return self._get_property("expiration_date")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_transfer_date(self):
|
||||||
|
return self._get_property("last_transfer_date")
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=253,
|
||||||
|
blank=False,
|
||||||
|
default=None, # prevent saving without a value
|
||||||
|
help_text="Fully qualified domain name",
|
||||||
|
)
|
||||||
|
|
||||||
|
# we use `is_active` rather than `domain_application.status`
|
||||||
|
# because domains may exist without associated applications
|
||||||
|
is_active = FSMField(
|
||||||
|
choices=[
|
||||||
|
(True, "Yes"),
|
||||||
|
(False, "No"),
|
||||||
|
],
|
||||||
|
default=False,
|
||||||
|
protected=True,
|
||||||
|
help_text="Domain is live in the registry",
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: determine the relationship between this field
|
||||||
|
# and the domain application's `creator` and `submitter`
|
||||||
|
owners = models.ManyToManyField(
|
||||||
|
User,
|
||||||
|
help_text="",
|
||||||
|
)
|
|
@ -147,12 +147,12 @@ class DomainApplication(TimeStampedModel):
|
||||||
related_name="current+",
|
related_name="current+",
|
||||||
)
|
)
|
||||||
|
|
||||||
requested_domain = models.ForeignKey(
|
requested_domain = models.OneToOneField(
|
||||||
Website,
|
"Domain",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="The requested domain",
|
help_text="The requested domain",
|
||||||
related_name="requested+",
|
related_name="domain_application",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
alternative_domains = models.ManyToManyField(
|
alternative_domains = models.ManyToManyField(
|
||||||
|
|
30
src/registrar/models/host.py
Normal file
30
src/registrar/models/host.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
from .domain import Domain
|
||||||
|
|
||||||
|
class Host(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
Hosts are internet-connected computers.
|
||||||
|
|
||||||
|
They may handle email, serve websites, or perform other tasks.
|
||||||
|
|
||||||
|
The registry is the source of truth for this data.
|
||||||
|
|
||||||
|
This model exists ONLY to allow a new registrant to draft DNS entries
|
||||||
|
before their application is approved.
|
||||||
|
"""
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=253,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default=None, # prevent saving without a value
|
||||||
|
unique=True,
|
||||||
|
help_text="Fully qualified domain name",
|
||||||
|
)
|
||||||
|
|
||||||
|
domain = models.ForeignKey(
|
||||||
|
Domain,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
help_text="Domain to which this host belongs",
|
||||||
|
)
|
30
src/registrar/models/host_ip.py
Normal file
30
src/registrar/models/host_ip.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.core.validators import validate_ipv46_address
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
from .host import Host
|
||||||
|
|
||||||
|
class HostIP(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
Hosts may have one or more IP addresses.
|
||||||
|
|
||||||
|
The registry is the source of truth for this data.
|
||||||
|
|
||||||
|
This model exists ONLY to allow a new registrant to draft DNS entries
|
||||||
|
before their application is approved.
|
||||||
|
"""
|
||||||
|
address = models.CharField(
|
||||||
|
max_length=46,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default=None, # prevent saving without a value
|
||||||
|
validators=[validate_ipv46_address],
|
||||||
|
help_text="IP address",
|
||||||
|
)
|
||||||
|
|
||||||
|
host = models.ForeignKey(
|
||||||
|
Host,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
help_text="Host to which this IP address belongs",
|
||||||
|
)
|
||||||
|
|
12
src/registrar/models/nameserver.py
Normal file
12
src/registrar/models/nameserver.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from .host import Host
|
||||||
|
|
||||||
|
class Nameserver(Host):
|
||||||
|
"""
|
||||||
|
A nameserver is a host which has been delegated to respond to DNS queries.
|
||||||
|
|
||||||
|
The registry is the source of truth for this data.
|
||||||
|
|
||||||
|
This model exists ONLY to allow a new registrant to draft DNS entries
|
||||||
|
before their application is approved.
|
||||||
|
"""
|
||||||
|
pass
|
|
@ -1,5 +1,3 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,26 +14,5 @@ class Website(models.Model):
|
||||||
help_text="",
|
help_text="",
|
||||||
)
|
)
|
||||||
|
|
||||||
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
|
||||||
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
|
||||||
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def string_could_be_domain(cls, domain: str) -> bool:
|
|
||||||
"""Return True if the string could be a domain name, otherwise False.
|
|
||||||
|
|
||||||
TODO: when we have a Domain class, this could be a classmethod there.
|
|
||||||
"""
|
|
||||||
if cls.DOMAIN_REGEX.match(domain):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def could_be_domain(self) -> bool:
|
|
||||||
"""Could this instance be a domain?"""
|
|
||||||
# short-circuit if self.website is null/None
|
|
||||||
if not self.website:
|
|
||||||
return False
|
|
||||||
return self.string_could_be_domain(str(self.website))
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.website)
|
return str(self.website)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
from registrar.models import Contact, DomainApplication, User, Website
|
from registrar.models import Contact, DomainApplication, User, Website, Domain
|
||||||
|
|
||||||
|
|
||||||
class TestDomainApplication(TestCase):
|
class TestDomainApplication(TestCase):
|
||||||
|
@ -22,6 +22,7 @@ class TestDomainApplication(TestCase):
|
||||||
contact = Contact.objects.create()
|
contact = Contact.objects.create()
|
||||||
com_website, _ = Website.objects.get_or_create(website="igorville.com")
|
com_website, _ = Website.objects.get_or_create(website="igorville.com")
|
||||||
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
|
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
application = DomainApplication.objects.create(
|
application = DomainApplication.objects.create(
|
||||||
creator=user,
|
creator=user,
|
||||||
investigator=user,
|
investigator=user,
|
||||||
|
@ -35,7 +36,7 @@ class TestDomainApplication(TestCase):
|
||||||
state_territory="CA",
|
state_territory="CA",
|
||||||
zip_code="12345-6789",
|
zip_code="12345-6789",
|
||||||
authorizing_official=contact,
|
authorizing_official=contact,
|
||||||
requested_domain=gov_website,
|
requested_domain=domain,
|
||||||
submitter=contact,
|
submitter=contact,
|
||||||
purpose="Igorville rules!",
|
purpose="Igorville rules!",
|
||||||
security_email="security@igorville.gov",
|
security_email="security@igorville.gov",
|
||||||
|
@ -56,9 +57,50 @@ class TestDomainApplication(TestCase):
|
||||||
|
|
||||||
def test_status_fsm_submit_succeed(self):
|
def test_status_fsm_submit_succeed(self):
|
||||||
user, _ = User.objects.get_or_create()
|
user, _ = User.objects.get_or_create()
|
||||||
site = Website.objects.create(website="igorville.gov")
|
site = Domain.objects.create(name="igorville.gov")
|
||||||
application = DomainApplication.objects.create(
|
application = DomainApplication.objects.create(
|
||||||
creator=user, requested_domain=site
|
creator=user, requested_domain=site
|
||||||
)
|
)
|
||||||
application.submit()
|
application.submit()
|
||||||
self.assertEqual(application.status, application.SUBMITTED)
|
self.assertEqual(application.status, application.SUBMITTED)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDomain(TestCase):
|
||||||
|
def test_empty_create_fails(self):
|
||||||
|
"""Can't create a completely empty domain."""
|
||||||
|
with self.assertRaisesRegex(IntegrityError, "name"):
|
||||||
|
Domain.objects.create()
|
||||||
|
|
||||||
|
def test_minimal_create(self):
|
||||||
|
"""Can create with just a name."""
|
||||||
|
domain = Domain.objects.create(name="igorville.gov")
|
||||||
|
self.assertEquals(domain.is_active, False)
|
||||||
|
|
||||||
|
def test_get_status(self):
|
||||||
|
"""Returns proper status based on `is_active`."""
|
||||||
|
domain = Domain.objects.create(name="igorville.gov")
|
||||||
|
domain.save()
|
||||||
|
self.assertEquals(None, domain.status)
|
||||||
|
domain.activate()
|
||||||
|
domain.save()
|
||||||
|
self.assertIn("ok", domain.status)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fsm_activate_fail_unique(self):
|
||||||
|
# can't activate domain if name is not unique
|
||||||
|
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
d2, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
d1.activate()
|
||||||
|
d1.save()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
d2.activate()
|
||||||
|
|
||||||
|
def test_fsm_activate_fail_unapproved(self):
|
||||||
|
# can't activate domain if application isn't approved
|
||||||
|
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
user, _ = User.objects.get_or_create()
|
||||||
|
application = DomainApplication.objects.create(creator=user)
|
||||||
|
d1.domain_application = application
|
||||||
|
d1.save()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
d1.activate()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue