mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-06 01:35:22 +02:00
Merge branch 'main' into nmb/543-login-required-middleware
This commit is contained in:
commit
7e2f2ec7e5
9 changed files with 372 additions and 236 deletions
|
@ -28,8 +28,7 @@ information to make connections between registry users and the domains that
|
|||
they manage. The registrar stores very few fields about a domain except for
|
||||
its name, so it could be straightforward to import the exported list of domains
|
||||
from Verisign's `escrow_domains.daily.dotgov.GOV.txt`. It doesn't appear that
|
||||
that table stores a flag for active or inactive, so every domain in the file
|
||||
can be imported into our system as `is_active=True`.
|
||||
that table stores a flag for active or inactive.
|
||||
|
||||
An example Django management command that can load the delimited text file
|
||||
from the daily escrow is in
|
||||
|
|
|
@ -25,6 +25,7 @@ django-phonenumber-field = {extras = ["phonenumberslite"], version = "*"}
|
|||
boto3 = "*"
|
||||
typing-extensions ='*'
|
||||
django-login-required-middleware = "*"
|
||||
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
|
||||
|
||||
[dev-packages]
|
||||
django-debug-toolbar = "*"
|
||||
|
|
115
src/Pipfile.lock
generated
115
src/Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "187278933085d8c5448015e2d28c23934c2098f4aadd82157379174629d8cf6b"
|
||||
"sha256": "1242c67b31261243a35128410d4a928fca3729ddac13b8c8e25adf31445c6328"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
|
@ -351,6 +351,10 @@
|
|||
"index": "pypi",
|
||||
"version": "==18.10.0"
|
||||
},
|
||||
"fred-epplib": {
|
||||
"git": "https://github.com/cisagov/epplib.git",
|
||||
"ref": "f818cbf0b069a12f03e1d72e4b9f4900924b832d"
|
||||
},
|
||||
"furl": {
|
||||
"hashes": [
|
||||
"sha256:5a6188fe2666c484a12159c18be97a1977a71d632ef5bb867ef15f54af39cc4e",
|
||||
|
@ -389,6 +393,89 @@
|
|||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7",
|
||||
"sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726",
|
||||
"sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03",
|
||||
"sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140",
|
||||
"sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a",
|
||||
"sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05",
|
||||
"sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03",
|
||||
"sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419",
|
||||
"sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4",
|
||||
"sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e",
|
||||
"sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67",
|
||||
"sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50",
|
||||
"sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894",
|
||||
"sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf",
|
||||
"sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947",
|
||||
"sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1",
|
||||
"sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd",
|
||||
"sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3",
|
||||
"sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92",
|
||||
"sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3",
|
||||
"sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457",
|
||||
"sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74",
|
||||
"sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf",
|
||||
"sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1",
|
||||
"sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4",
|
||||
"sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975",
|
||||
"sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5",
|
||||
"sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe",
|
||||
"sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7",
|
||||
"sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1",
|
||||
"sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2",
|
||||
"sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409",
|
||||
"sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f",
|
||||
"sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f",
|
||||
"sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5",
|
||||
"sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24",
|
||||
"sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e",
|
||||
"sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4",
|
||||
"sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a",
|
||||
"sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c",
|
||||
"sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de",
|
||||
"sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f",
|
||||
"sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b",
|
||||
"sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5",
|
||||
"sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7",
|
||||
"sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a",
|
||||
"sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c",
|
||||
"sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9",
|
||||
"sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e",
|
||||
"sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab",
|
||||
"sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941",
|
||||
"sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5",
|
||||
"sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45",
|
||||
"sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7",
|
||||
"sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892",
|
||||
"sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746",
|
||||
"sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c",
|
||||
"sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53",
|
||||
"sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe",
|
||||
"sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184",
|
||||
"sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38",
|
||||
"sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df",
|
||||
"sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9",
|
||||
"sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b",
|
||||
"sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2",
|
||||
"sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0",
|
||||
"sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda",
|
||||
"sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b",
|
||||
"sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5",
|
||||
"sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380",
|
||||
"sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33",
|
||||
"sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8",
|
||||
"sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1",
|
||||
"sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889",
|
||||
"sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9",
|
||||
"sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f",
|
||||
"sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.9.2"
|
||||
},
|
||||
"mako": {
|
||||
"hashes": [
|
||||
"sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818",
|
||||
|
@ -711,11 +798,11 @@
|
|||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c",
|
||||
"sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"
|
||||
"sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26",
|
||||
"sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.6.2"
|
||||
"version": "==4.6.3"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
|
@ -871,19 +958,19 @@
|
|||
},
|
||||
"django-stubs": {
|
||||
"hashes": [
|
||||
"sha256:93baff824f0a056e71036b423b942a74f07b909e45e3fa38185b910f597c5c08",
|
||||
"sha256:d2c671989efb3f7b0fa91e461909ad5a5a52155fe7fe6d1f2058cb88e3afb123"
|
||||
"sha256:66477bdba25407623f4079205e58f3c7265a4f0d8f7c9f540a6edc16f8883a5b",
|
||||
"sha256:8c15d5f7b05926805cfb25f2bfbf3509c37792fbd8aec5aedea358b85d8bccd5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.0"
|
||||
"version": "==4.2.1"
|
||||
},
|
||||
"django-stubs-ext": {
|
||||
"hashes": [
|
||||
"sha256:55b2e3077f883e0131a7596f8ff8b19f8fc3ca325a3318ccacf5331acb2601e4",
|
||||
"sha256:7789f0caeca7152fef07ad6b94dec7310a05d0b8dab77f7979e19db0037b5127"
|
||||
"sha256:2696d6f7d8538341b060cffa9565c72ea797e866687e040b86d29cad8799e5fe",
|
||||
"sha256:4b6b63e49f4ba30d93ec46f87507648c99c9de6911e651ad69db7084fd5b2f4e"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.2.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.2.1"
|
||||
},
|
||||
"django-webtest": {
|
||||
"hashes": [
|
||||
|
@ -1226,11 +1313,11 @@
|
|||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c",
|
||||
"sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"
|
||||
"sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26",
|
||||
"sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.6.2"
|
||||
"version": "==4.6.3"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 4.2.1 on 2023-06-01 21:47
|
||||
|
||||
from django.db import migrations
|
||||
import django_fsm # type: ignore
|
||||
import registrar.models.utility.domain_field
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0024_alter_contact_email"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="domain",
|
||||
name="unique_domain_name_in_registry",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="domain",
|
||||
name="is_active",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domain",
|
||||
name="state",
|
||||
field=django_fsm.FSMField(
|
||||
choices=[
|
||||
("created", "Created"),
|
||||
("deleted", "Deleted"),
|
||||
("unknown", "Unknown"),
|
||||
],
|
||||
default="unknown",
|
||||
help_text="Very basic info about the lifecycle of this domain object",
|
||||
max_length=21,
|
||||
protected=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domain",
|
||||
name="name",
|
||||
field=registrar.models.utility.domain_field.DomainField(
|
||||
default=None,
|
||||
help_text="Fully qualified domain name",
|
||||
max_length=253,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,42 +1,45 @@
|
|||
import logging
|
||||
import re
|
||||
|
||||
from typing import List
|
||||
from datetime import date
|
||||
from django_fsm import FSMField # type: ignore
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django_fsm import FSMField, transition # type: ignore
|
||||
|
||||
from api.views import in_domains
|
||||
from registrar.utility import errors
|
||||
from epplibwrapper import (
|
||||
CLIENT as registry,
|
||||
commands,
|
||||
)
|
||||
|
||||
from .utility.domain_field import DomainField
|
||||
from .utility.domain_helper import DomainHelper
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
from .public_contact import PublicContact
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Domain(TimeStampedModel):
|
||||
class Domain(TimeStampedModel, DomainHelper):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
DNS entries in the registry
|
||||
|
||||
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",
|
||||
),
|
||||
]
|
||||
~~~ HOW TO USE THIS CLASS ~~~
|
||||
|
||||
A) You can create a Domain object with just a name. `Domain(name="something.gov")`.
|
||||
B) Saving the Domain object will not contact the registry, as it may be useful
|
||||
to have Domain objects in an `UNKNOWN` pre-created state.
|
||||
C) Domain properties are lazily loaded. Accessing `my_domain.expiration_date` will
|
||||
contact the registry, if a cached copy does not exist.
|
||||
D) Domain creation is lazy. If `my_domain.expiration_date` finds that `my_domain`
|
||||
does not exist in the registry, it will ask the registry to create it.
|
||||
F) Created is _not_ the same as active aka live on the internet.
|
||||
G) Activation is controlled by the registry. It will happen automatically when the
|
||||
domain meets the required checks.
|
||||
"""
|
||||
|
||||
class Status(models.TextChoices):
|
||||
"""
|
||||
|
@ -91,221 +94,202 @@ class Domain(TimeStampedModel):
|
|||
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}$")
|
||||
class State(models.TextChoices):
|
||||
"""These capture (some of) the states a domain object can be in."""
|
||||
|
||||
@classmethod
|
||||
def string_could_be_domain(cls, domain: str | None) -> bool:
|
||||
"""Return True if the string could be a domain name, otherwise False."""
|
||||
if not isinstance(domain, str):
|
||||
return False
|
||||
return bool(cls.DOMAIN_REGEX.match(domain))
|
||||
# the normal state of a domain object -- may or may not be active!
|
||||
CREATED = "created"
|
||||
|
||||
@classmethod
|
||||
def validate(cls, domain: str | None, blank_ok=False) -> str:
|
||||
"""Attempt to determine if a domain name could be requested."""
|
||||
if domain is None:
|
||||
raise errors.BlankValueError()
|
||||
if not isinstance(domain, str):
|
||||
raise ValueError("Domain name must be a string")
|
||||
domain = domain.lower().strip()
|
||||
if domain == "":
|
||||
if blank_ok:
|
||||
return domain
|
||||
else:
|
||||
raise errors.BlankValueError()
|
||||
if domain.endswith(".gov"):
|
||||
domain = domain[:-4]
|
||||
if "." in domain:
|
||||
raise errors.ExtraDotsError()
|
||||
if not Domain.string_could_be_domain(domain + ".gov"):
|
||||
raise ValueError()
|
||||
if in_domains(domain):
|
||||
raise errors.DomainUnavailableError()
|
||||
return domain
|
||||
# previously existed but has been deleted from the registry
|
||||
DELETED = "deleted"
|
||||
|
||||
# the state is indeterminate
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
@classmethod
|
||||
def available(cls, domain: str) -> bool:
|
||||
"""Check if a domain is available.
|
||||
"""Check if a domain is available."""
|
||||
if not cls.string_could_be_domain(domain):
|
||||
raise ValueError("Not a valid domain: %s" % str(domain))
|
||||
req = commands.CheckDomain([domain])
|
||||
return registry.send(req).res_data[0].avail
|
||||
|
||||
Not implemented. Returns a dummy value for testing."""
|
||||
return False # domain_check(domain)
|
||||
@classmethod
|
||||
def registered(cls, domain: str) -> bool:
|
||||
"""Check if a domain is _not_ available."""
|
||||
return not cls.available(domain)
|
||||
|
||||
@property
|
||||
def contacts(self) -> dict[str, str]:
|
||||
"""
|
||||
Get a dictionary of registry IDs for the contacts for this domain.
|
||||
|
||||
IDs are provided as strings, e.g.
|
||||
|
||||
{ PublicContact.ContactTypeChoices.REGISTRANT: "jd1234",
|
||||
PublicContact.ContactTypeChoices.ADMINISTRATIVE: "sh8013",...}
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def creation_date(self) -> date:
|
||||
"""Get the `cr_date` element from the registry."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def last_transferred_date(self) -> date:
|
||||
"""Get the `tr_date` element from the registry."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def last_updated_date(self) -> date:
|
||||
"""Get the `up_date` element from the registry."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def expiration_date(self) -> date:
|
||||
"""Get or set the `ex_date` element from the registry."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@expiration_date.setter # type: ignore
|
||||
def expiration_date(self, ex_date: date):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def password(self) -> str:
|
||||
"""
|
||||
Get the `auth_info.pw` element from the registry. Not a real password.
|
||||
|
||||
This `auth_info` element is required by the EPP protocol, but the registry is
|
||||
using a different mechanism to ensure unauthorized clients cannot perform
|
||||
actions on domains they do not own. This field provides no security features.
|
||||
It is not a secret.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def nameservers(self) -> list[tuple[str]]:
|
||||
"""
|
||||
Get or set a complete list of nameservers for this domain.
|
||||
|
||||
Hosts are provided as a list of tuples, e.g.
|
||||
|
||||
[("ns1.example.com",), ("ns1.example.gov", "0.0.0.0")]
|
||||
|
||||
Subordinate hosts (something.your-domain.gov) MUST have IP addresses,
|
||||
while non-subordinate hosts MUST NOT.
|
||||
"""
|
||||
# TODO: call EPP to get this info instead of returning fake data.
|
||||
return [
|
||||
("ns1.example.com",),
|
||||
("ns2.example.com",),
|
||||
("ns3.example.com",),
|
||||
]
|
||||
|
||||
@nameservers.setter # type: ignore
|
||||
def nameservers(self, hosts: list[tuple[str]]):
|
||||
# TODO: call EPP to set this info.
|
||||
pass
|
||||
|
||||
@property
|
||||
def statuses(self) -> list[str]:
|
||||
"""
|
||||
Get or set the domain `status` elements from the registry.
|
||||
|
||||
A domain's status indicates various properties. See Domain.Status.
|
||||
"""
|
||||
# implementation note: the Status object from EPP stores the string in
|
||||
# a dataclass property `state`, not to be confused with the `state` field here
|
||||
raise NotImplementedError()
|
||||
|
||||
@statuses.setter # type: ignore
|
||||
def statuses(self, statuses: list[str]):
|
||||
# TODO: there are a long list of rules in the RFC about which statuses
|
||||
# can be combined; check that here and raise errors for invalid combinations -
|
||||
# some statuses cannot be set by the client at all
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def registrant_contact(self) -> PublicContact:
|
||||
"""Get or set the registrant for this domain."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@registrant_contact.setter # type: ignore
|
||||
def registrant_contact(self, contact: PublicContact):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def administrative_contact(self) -> PublicContact:
|
||||
"""Get or set the admin contact for this domain."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@administrative_contact.setter # type: ignore
|
||||
def administrative_contact(self, contact: PublicContact):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def security_contact(self) -> PublicContact:
|
||||
"""Get or set the security contact for this domain."""
|
||||
# TODO: replace this with a real implementation
|
||||
contact = PublicContact.get_default_security()
|
||||
contact.domain = self
|
||||
contact.email = "mayor@igorville.gov"
|
||||
return contact
|
||||
|
||||
@security_contact.setter # type: ignore
|
||||
def security_contact(self, contact: PublicContact):
|
||||
# TODO: replace this with a real implementation
|
||||
pass
|
||||
|
||||
@property
|
||||
def technical_contact(self) -> PublicContact:
|
||||
"""Get or set the tech contact for this domain."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@technical_contact.setter # type: ignore
|
||||
def technical_contact(self, contact: PublicContact):
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Is the domain live on the inter webs?"""
|
||||
# TODO: implement a check -- should be performant so it can be called for
|
||||
# any number of domains on a status page
|
||||
# this is NOT as simple as checking if Domain.Status.OK is in self.statuses
|
||||
return False
|
||||
|
||||
def transfer(self):
|
||||
"""Going somewhere. Not implemented."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def renew(self):
|
||||
"""Time to renew. Not implemented."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
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"):
|
||||
if property in self.info:
|
||||
return self.info[property]
|
||||
else:
|
||||
raise KeyError(
|
||||
"Requested key %s was not found in registry data." % str(property)
|
||||
)
|
||||
else:
|
||||
# TODO: return an error if registry cannot be contacted
|
||||
return None
|
||||
def place_client_hold(self):
|
||||
"""This domain should not be active."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@transition(field="is_active", source="*", target=True)
|
||||
def activate(self):
|
||||
"""This domain should be made live."""
|
||||
DomainApplication = apps.get_model("registrar.DomainApplication")
|
||||
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.")
|
||||
|
||||
@property
|
||||
def sld(self):
|
||||
"""Get or set the second level domain string."""
|
||||
return self.name.split(".")[0]
|
||||
|
||||
@sld.setter
|
||||
def sld(self, value: str):
|
||||
parts = self.name.split(".")
|
||||
tld = parts[1] if len(parts) > 1 else ""
|
||||
if Domain.string_could_be_domain(f"{value}.{tld}"):
|
||||
self.name = f"{value}.{tld}"
|
||||
else:
|
||||
raise ValidationError("%s is not a valid second level domain" % value)
|
||||
|
||||
@property
|
||||
def tld(self):
|
||||
"""Get or set the top level domain string."""
|
||||
parts = self.name.split(".")
|
||||
return parts[1] if len(parts) > 1 else ""
|
||||
|
||||
@tld.setter
|
||||
def tld(self, value: str):
|
||||
sld = self.name.split(".")[0]
|
||||
if Domain.string_could_be_domain(f"{sld}.{value}"):
|
||||
self.name = f"{sld}.{value}"
|
||||
else:
|
||||
raise ValidationError("%s is not a valid top level domain" % value)
|
||||
def remove_client_hold(self):
|
||||
"""This domain is okay to be active."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def nameservers(self) -> List[str]:
|
||||
"""A list of the nameservers for this domain.
|
||||
|
||||
TODO: call EPP to get this info instead of returning fake data.
|
||||
"""
|
||||
return [
|
||||
# reserved example domain
|
||||
"ns1.example.com",
|
||||
"ns2.example.com",
|
||||
"ns3.example.com",
|
||||
]
|
||||
|
||||
def set_nameservers(self, new_nameservers: List[str]):
|
||||
"""Set the nameservers for this domain."""
|
||||
# TODO: call EPP to set these values in the registry instead of doing
|
||||
# nothing.
|
||||
logger.warn("TODO: Fake setting nameservers to %s", new_nameservers)
|
||||
|
||||
def security_email(self) -> str:
|
||||
"""Get the security email for this domain.
|
||||
|
||||
TODO: call EPP to get this info instead of returning fake data.
|
||||
"""
|
||||
return "mayor@igorville.gov"
|
||||
|
||||
def set_security_email(self, new_security_email: str):
|
||||
"""Set the security email for this domain."""
|
||||
# TODO: call EPP to set these values in the registry instead of doing
|
||||
# nothing.
|
||||
logger.warn("TODO: Fake setting security email to %s", new_security_email)
|
||||
|
||||
@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(
|
||||
name = DomainField(
|
||||
max_length=253,
|
||||
blank=False,
|
||||
default=None, # prevent saving without a value
|
||||
unique=True,
|
||||
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,
|
||||
# TODO: how to edit models in Django admin if protected = True
|
||||
protected=False,
|
||||
help_text="Domain is live in the registry",
|
||||
state = FSMField(
|
||||
max_length=21,
|
||||
choices=State.choices,
|
||||
default=State.UNKNOWN,
|
||||
protected=True, # cannot change state directly, particularly in Django admin
|
||||
help_text="Very basic info about the lifecycle of this domain object",
|
||||
)
|
||||
|
||||
# ForeignKey on UserDomainRole creates a "permissions" member for
|
||||
|
|
|
@ -32,7 +32,7 @@ class PublicContact(TimeStampedModel):
|
|||
if hasattr(self, "domain"):
|
||||
match self.contact_type:
|
||||
case PublicContact.ContactTypeChoices.REGISTRANT:
|
||||
self.domain.registrant = self
|
||||
self.domain.registrant_contact = self
|
||||
case PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
||||
self.domain.administrative_contact = self
|
||||
case PublicContact.ContactTypeChoices.TECHNICAL:
|
||||
|
|
13
src/registrar/models/utility/domain_field.py
Normal file
13
src/registrar/models/utility/domain_field.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class DomainField(models.CharField):
|
||||
"""Subclass of CharField to enforce domain name specific requirements."""
|
||||
|
||||
def to_python(self, value):
|
||||
"""Convert to lowercase during deserialization and during form `clean`."""
|
||||
if value is None:
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return value.lower()
|
||||
return str(value).lower()
|
|
@ -93,7 +93,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
|||
def get_initial(self):
|
||||
"""The initial value for the form (which is a formset here)."""
|
||||
domain = self.get_object()
|
||||
return [{"server": server} for server in domain.nameservers()]
|
||||
return [{"server": name} for name, *ip in domain.nameservers]
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the nameservers page for the domain."""
|
||||
|
@ -134,12 +134,13 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
|||
nameservers = []
|
||||
for form in formset:
|
||||
try:
|
||||
nameservers.append(form.cleaned_data["server"])
|
||||
as_tuple = (form.cleaned_data["server"],)
|
||||
nameservers.append(as_tuple)
|
||||
except KeyError:
|
||||
# no server information in this field, skip it
|
||||
pass
|
||||
domain = self.get_object()
|
||||
domain.set_nameservers(nameservers)
|
||||
domain.nameservers = nameservers
|
||||
|
||||
messages.success(
|
||||
self.request, "The name servers for this domain have been updated."
|
||||
|
@ -199,7 +200,7 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
|||
"""The initial value for the form."""
|
||||
domain = self.get_object()
|
||||
initial = super().get_initial()
|
||||
initial["security_email"] = domain.security_email()
|
||||
initial["security_email"] = domain.security_contact.email
|
||||
return initial
|
||||
|
||||
def get_success_url(self):
|
||||
|
@ -222,7 +223,9 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
|||
# Set the security email from the form
|
||||
new_email = form.cleaned_data.get("security_email", "")
|
||||
domain = self.get_object()
|
||||
domain.set_security_email(new_email)
|
||||
contact = domain.security_contact
|
||||
contact.email = new_email
|
||||
contact.save()
|
||||
|
||||
messages.success(
|
||||
self.request, "The security email for this domain have been updated."
|
||||
|
|
|
@ -22,11 +22,13 @@ django-phonenumber-field[phonenumberslite]==7.1.0
|
|||
django-widget-tweaks==1.4.12
|
||||
environs[django]==9.5.0
|
||||
faker==18.10.0
|
||||
git+https://github.com/cisagov/epplib.git@f818cbf0b069a12f03e1d72e4b9f4900924b832d#egg=fred-epplib
|
||||
furl==2.1.3
|
||||
future==0.18.3 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
gunicorn==20.1.0
|
||||
idna==3.4 ; python_version >= '3.5'
|
||||
jmespath==1.0.1 ; python_version >= '3.7'
|
||||
lxml==4.9.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
mako==1.2.4 ; python_version >= '3.7'
|
||||
markupsafe==2.1.2 ; python_version >= '3.7'
|
||||
marshmallow==3.19.0 ; python_version >= '3.7'
|
||||
|
@ -46,6 +48,6 @@ s3transfer==0.6.1 ; python_version >= '3.7'
|
|||
setuptools==67.8.0 ; python_version >= '3.7'
|
||||
six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
sqlparse==0.4.4 ; python_version >= '3.5'
|
||||
typing-extensions==4.6.2
|
||||
typing-extensions==4.6.3
|
||||
urllib3==1.26.16 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
whitenoise==6.4.0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue